1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2012, 2013 Zimbra Software, LLC. 5 * 6 * The contents of this file are subject to the Zimbra Public License 7 * Version 1.4 ("License"); you may not use this file except in 8 * compliance with the License. You may obtain a copy of the License at 9 * http://www.zimbra.com/license. 10 * 11 * Software distributed under the License is distributed on an "AS IS" 12 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. 13 * ***** END LICENSE BLOCK ***** 14 */ 15 // FILE IS GENERATED BY COMBINING THE SOURCES IN THE "classes" DIRECTORY SO DON'T MODIFY THIS FILE DIRECTLY 16 (function(win) { 17 var whiteSpaceRe = /^\s*|\s*$/g, 18 undef, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1'; 19 20 var tinymce = { 21 majorVersion : '3', 22 23 minorVersion : '5.4.1', 24 25 releaseDate : '2012-06-24', 26 27 _init : function() { 28 var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; 29 30 t.isOpera = win.opera && opera.buildNumber; 31 32 t.isWebKit = /WebKit/.test(ua); 33 34 t.isIE = !t.isWebKit && !t.isOpera && (/MSIE/gi).test(ua) && (/Explorer/gi).test(na.appName); 35 36 t.isIE6 = t.isIE && /MSIE [56]/.test(ua); 37 38 t.isIE7 = t.isIE && /MSIE [7]/.test(ua); 39 40 t.isIE8 = t.isIE && /MSIE [8]/.test(ua); 41 42 t.isIE9 = t.isIE && /MSIE [9]/.test(ua); 43 44 t.isGecko = !t.isWebKit && /Gecko/.test(ua); 45 46 t.isMac = ua.indexOf('Mac') != -1; 47 48 t.isAir = /adobeair/i.test(ua); 49 50 t.isIDevice = /(iPad|iPhone)/.test(ua); 51 52 t.isIOS5 = t.isIDevice && ua.match(/AppleWebKit\/(\d*)/)[1]>=534; 53 54 // TinyMCE .NET webcontrol might be setting the values for TinyMCE 55 if (win.tinyMCEPreInit) { 56 t.suffix = tinyMCEPreInit.suffix; 57 t.baseURL = tinyMCEPreInit.base; 58 t.query = tinyMCEPreInit.query; 59 return; 60 } 61 62 // Get suffix and base 63 t.suffix = ''; 64 65 // If base element found, add that infront of baseURL 66 nl = d.getElementsByTagName('base'); 67 for (i=0; i<nl.length; i++) { 68 v = nl[i].href; 69 if (v) { 70 // Host only value like http://site.com or http://site.com:8008 71 if (/^https?:\/\/[^\/]+$/.test(v)) 72 v += '/'; 73 74 base = v ? v.match(/.*\//)[0] : ''; // Get only directory 75 } 76 } 77 78 function getBase(n) { 79 if (n.src && /tiny_mce(|_gzip|_jquery|_prototype|_full)(_dev|_src)?.js/.test(n.src)) { 80 if (/_(src|dev)\.js/g.test(n.src)) 81 t.suffix = '_src'; 82 83 if ((p = n.src.indexOf('?')) != -1) 84 t.query = n.src.substring(p + 1); 85 86 t.baseURL = n.src.substring(0, n.src.lastIndexOf('/')); 87 88 // If path to script is relative and a base href was found add that one infront 89 // the src property will always be an absolute one on non IE browsers and IE 8 90 // so this logic will basically only be executed on older IE versions 91 if (base && t.baseURL.indexOf('://') == -1 && t.baseURL.indexOf('/') !== 0) 92 t.baseURL = base + t.baseURL; 93 94 return t.baseURL; 95 } 96 97 return null; 98 }; 99 100 // Check document 101 nl = d.getElementsByTagName('script'); 102 for (i=0; i<nl.length; i++) { 103 if (getBase(nl[i])) 104 return; 105 } 106 107 // Check head 108 n = d.getElementsByTagName('head')[0]; 109 if (n) { 110 nl = n.getElementsByTagName('script'); 111 for (i=0; i<nl.length; i++) { 112 if (getBase(nl[i])) 113 return; 114 } 115 } 116 117 return; 118 }, 119 120 is : function(o, t) { 121 if (!t) 122 return o !== undef; 123 124 if (t == 'array' && (o.hasOwnProperty && o instanceof Array)) 125 return true; 126 127 return typeof(o) == t; 128 }, 129 130 makeMap : function(items, delim, map) { 131 var i; 132 133 items = items || []; 134 delim = delim || ','; 135 136 if (typeof(items) == "string") 137 items = items.split(delim); 138 139 map = map || {}; 140 141 i = items.length; 142 while (i--) 143 map[items[i]] = {}; 144 145 return map; 146 }, 147 148 each : function(o, cb, s) { 149 var n, l; 150 151 if (!o) 152 return 0; 153 154 s = s || o; 155 156 if (o.length !== undef) { 157 // Indexed arrays, needed for Safari 158 for (n=0, l = o.length; n < l; n++) { 159 if (cb.call(s, o[n], n, o) === false) 160 return 0; 161 } 162 } else { 163 // Hashtables 164 for (n in o) { 165 if (o.hasOwnProperty(n)) { 166 if (cb.call(s, o[n], n, o) === false) 167 return 0; 168 } 169 } 170 } 171 172 return 1; 173 }, 174 175 176 trim : function(s) { 177 return (s ? '' + s : '').replace(whiteSpaceRe, ''); 178 }, 179 180 create : function(s, p, root) { 181 var t = this, sp, ns, cn, scn, c, de = 0; 182 183 // Parse : <prefix> <class>:<super class> 184 s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s); 185 cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name 186 187 // Create namespace for new class 188 ns = t.createNS(s[3].replace(/\.\w+$/, ''), root); 189 190 // Class already exists 191 if (ns[cn]) 192 return; 193 194 // Make pure static class 195 if (s[2] == 'static') { 196 ns[cn] = p; 197 198 if (this.onCreate) 199 this.onCreate(s[2], s[3], ns[cn]); 200 201 return; 202 } 203 204 // Create default constructor 205 if (!p[cn]) { 206 p[cn] = function() {}; 207 de = 1; 208 } 209 210 // Add constructor and methods 211 ns[cn] = p[cn]; 212 t.extend(ns[cn].prototype, p); 213 214 // Extend 215 if (s[5]) { 216 sp = t.resolve(s[5]).prototype; 217 scn = s[5].match(/\.(\w+)$/i)[1]; // Class name 218 219 // Extend constructor 220 c = ns[cn]; 221 if (de) { 222 // Add passthrough constructor 223 ns[cn] = function() { 224 return sp[scn].apply(this, arguments); 225 }; 226 } else { 227 // Add inherit constructor 228 ns[cn] = function() { 229 this.parent = sp[scn]; 230 return c.apply(this, arguments); 231 }; 232 } 233 ns[cn].prototype[cn] = ns[cn]; 234 235 // Add super methods 236 t.each(sp, function(f, n) { 237 ns[cn].prototype[n] = sp[n]; 238 }); 239 240 // Add overridden methods 241 t.each(p, function(f, n) { 242 // Extend methods if needed 243 if (sp[n]) { 244 ns[cn].prototype[n] = function() { 245 this.parent = sp[n]; 246 return f.apply(this, arguments); 247 }; 248 } else { 249 if (n != cn) 250 ns[cn].prototype[n] = f; 251 } 252 }); 253 } 254 255 // Add static methods 256 t.each(p['static'], function(f, n) { 257 ns[cn][n] = f; 258 }); 259 260 if (this.onCreate) 261 this.onCreate(s[2], s[3], ns[cn].prototype); 262 }, 263 264 walk : function(o, f, n, s) { 265 s = s || this; 266 267 if (o) { 268 if (n) 269 o = o[n]; 270 271 tinymce.each(o, function(o, i) { 272 if (f.call(s, o, i, n) === false) 273 return false; 274 275 tinymce.walk(o, f, n, s); 276 }); 277 } 278 }, 279 280 createNS : function(n, o) { 281 var i, v; 282 283 o = o || win; 284 285 n = n.split('.'); 286 for (i=0; i<n.length; i++) { 287 v = n[i]; 288 289 if (!o[v]) 290 o[v] = {}; 291 292 o = o[v]; 293 } 294 295 return o; 296 }, 297 298 resolve : function(n, o) { 299 var i, l; 300 301 o = o || win; 302 303 n = n.split('.'); 304 for (i = 0, l = n.length; i < l; i++) { 305 o = o[n[i]]; 306 307 if (!o) 308 break; 309 } 310 311 return o; 312 }, 313 314 addUnload : function(f, s) { 315 var t = this, unload; 316 317 unload = function() { 318 var li = t.unloads, o, n; 319 320 if (li) { 321 // Call unload handlers 322 for (n in li) { 323 o = li[n]; 324 325 if (o && o.func) 326 o.func.call(o.scope, 1); // Send in one arg to distinct unload and user destroy 327 } 328 329 // Detach unload function 330 if (win.detachEvent) { 331 win.detachEvent('onbeforeunload', fakeUnload); 332 win.detachEvent('onunload', unload); 333 } else if (win.removeEventListener) 334 win.removeEventListener('unload', unload, false); 335 336 // Destroy references 337 t.unloads = o = li = w = unload = 0; 338 339 // Run garbarge collector on IE 340 if (win.CollectGarbage) 341 CollectGarbage(); 342 } 343 }; 344 345 function fakeUnload() { 346 var d = document; 347 348 function stop() { 349 // Prevent memory leak 350 d.detachEvent('onstop', stop); 351 352 // Call unload handler 353 if (unload) 354 unload(); 355 356 d = 0; 357 }; 358 359 // Is there things still loading, then do some magic 360 if (d.readyState == 'interactive') { 361 // Fire unload when the currently loading page is stopped 362 if (d) 363 d.attachEvent('onstop', stop); 364 365 // Remove onstop listener after a while to prevent the unload function 366 // to execute if the user presses cancel in an onbeforeunload 367 // confirm dialog and then presses the browser stop button 368 win.setTimeout(function() { 369 if (d) 370 d.detachEvent('onstop', stop); 371 }, 0); 372 } 373 }; 374 375 f = {func : f, scope : s || this}; 376 377 if (!t.unloads) { 378 // Attach unload handler 379 if (win.attachEvent) { 380 win.attachEvent('onunload', unload); 381 win.attachEvent('onbeforeunload', fakeUnload); 382 } else if (win.addEventListener) 383 win.addEventListener('unload', unload, false); 384 385 // Setup initial unload handler array 386 t.unloads = [f]; 387 } else 388 t.unloads.push(f); 389 390 return f; 391 }, 392 393 removeUnload : function(f) { 394 var u = this.unloads, r = null; 395 396 tinymce.each(u, function(o, i) { 397 if (o && o.func == f) { 398 u.splice(i, 1); 399 r = f; 400 return false; 401 } 402 }); 403 404 return r; 405 }, 406 407 explode : function(s, d) { 408 if (!s || tinymce.is(s, 'array')) { 409 return s; 410 } 411 412 return tinymce.map(s.split(d || ','), tinymce.trim); 413 }, 414 415 _addVer : function(u) { 416 var v; 417 418 if (!this.query) 419 return u; 420 421 v = (u.indexOf('?') == -1 ? '?' : '&') + this.query; 422 423 if (u.indexOf('#') == -1) 424 return u + v; 425 426 return u.replace('#', v + '#'); 427 }, 428 429 // Fix function for IE 9 where regexps isn't working correctly 430 // Todo: remove me once MS fixes the bug 431 _replace : function(find, replace, str) { 432 // On IE9 we have to fake $x replacement 433 if (isRegExpBroken) { 434 return str.replace(find, function() { 435 var val = replace, args = arguments, i; 436 437 for (i = 0; i < args.length - 2; i++) { 438 if (args[i] === undef) { 439 val = val.replace(new RegExp('\\$' + i, 'g'), ''); 440 } else { 441 val = val.replace(new RegExp('\\$' + i, 'g'), args[i]); 442 } 443 } 444 445 return val; 446 }); 447 } 448 449 return str.replace(find, replace); 450 } 451 452 }; 453 454 // Initialize the API 455 tinymce._init(); 456 457 // Expose tinymce namespace to the global namespace (window) 458 win.tinymce = win.tinyMCE = tinymce; 459 460 // Describe the different namespaces 461 462 })(window); 463 464 465 (function($, tinymce) { 466 var is = tinymce.is, attrRegExp = /^(href|src|style)$/i, undef; 467 468 // jQuery is undefined 469 if (!$ && window.console) { 470 return console.log("Load jQuery first!"); 471 } 472 473 // Stick jQuery into the tinymce namespace 474 tinymce.$ = $; 475 476 // Setup adapter 477 tinymce.adapter = { 478 patchEditor : function(editor) { 479 var fn = $.fn; 480 481 // Adapt the css function to make sure that the data-mce-style 482 // attribute gets updated with the new style information 483 function css(name, value) { 484 var self = this; 485 486 // Remove data-mce-style when set operation occurs 487 if (value) 488 self.removeAttr('data-mce-style'); 489 490 return fn.css.apply(self, arguments); 491 }; 492 493 // Apapt the attr function to make sure that it uses the data-mce- prefixed variants 494 function attr(name, value) { 495 var self = this; 496 497 // Update/retrive data-mce- attribute variants 498 if (attrRegExp.test(name)) { 499 if (value !== undef) { 500 // Use TinyMCE behavior when setting the specifc attributes 501 self.each(function(i, node) { 502 editor.dom.setAttrib(node, name, value); 503 }); 504 505 return self; 506 } else 507 return self.attr('data-mce-' + name); 508 } 509 510 // Default behavior 511 return fn.attr.apply(self, arguments); 512 }; 513 514 // Patch various jQuery functions to handle tinymce specific attribute and content behavior 515 // we don't patch the jQuery.fn directly since it will most likely break compatibility 516 // with other jQuery logic on the page. Only instances created by TinyMCE should be patched. 517 function patch(jq) { 518 // Patch some functions, only patch the object once 519 if (jq.css !== css) { 520 // Patch css/attr to use the data-mce- prefixed attribute variants 521 jq.css = css; 522 jq.attr = attr; 523 524 jq.tinymce = editor; 525 526 // Each pushed jQuery instance needs to be patched 527 // as well for example when traversing the DOM 528 jq.pushStack = function() { 529 return patch(fn.pushStack.apply(this, arguments)); 530 }; 531 } 532 533 return jq; 534 }; 535 536 // Add a $ function on each editor instance this one is scoped for the editor document object 537 // this way you can do chaining like this tinymce.get(0).$('p').append('text').css('color', 'red'); 538 editor.$ = function(selector, scope) { 539 var doc = editor.getDoc(); 540 541 return patch($(selector || doc, doc || scope)); 542 }; 543 } 544 }; 545 546 // Patch in core NS functions 547 tinymce.extend = $.extend; 548 tinymce.extend(tinymce, { 549 map : $.map, 550 grep : function(a, f) {return $.grep(a, f || function(){return 1;});}, 551 inArray : function(a, v) {return $.inArray(v, a || []);} 552 553 /* Didn't iterate stylesheets 554 each : function(o, cb, s) { 555 if (!o) 556 return 0; 557 558 var r = 1; 559 560 $.each(o, function(nr, el){ 561 if (cb.call(s, el, nr, o) === false) { 562 r = 0; 563 return false; 564 } 565 }); 566 567 return r; 568 }*/ 569 }); 570 571 // Patch in functions in various clases 572 // Add a "#ifndefjquery" statement around each core API function you add below 573 var patches = { 574 'tinymce.dom.DOMUtils' : { 575 /* 576 addClass : function(e, c) { 577 if (is(e, 'array') && is(e[0], 'string')) 578 e = e.join(',#'); 579 return (e && $(is(e, 'string') ? '#' + e : e) 580 .addClass(c) 581 .attr('class')) || false; 582 }, 583 584 hasClass : function(n, c) { 585 return $(is(n, 'string') ? '#' + n : n).hasClass(c); 586 }, 587 588 removeClass : function(e, c) { 589 if (!e) 590 return false; 591 592 var r = []; 593 594 $(is(e, 'string') ? '#' + e : e) 595 .removeClass(c) 596 .each(function(){ 597 r.push(this.className); 598 }); 599 600 return r.length == 1 ? r[0] : r; 601 }, 602 */ 603 604 select : function(pattern, scope) { 605 var t = this; 606 607 return $.find(pattern, t.get(scope) || t.get(t.settings.root_element) || t.doc, []); 608 }, 609 610 is : function(n, patt) { 611 return $(this.get(n)).is(patt); 612 } 613 614 /* 615 show : function(e) { 616 if (is(e, 'array') && is(e[0], 'string')) 617 e = e.join(',#'); 618 619 $(is(e, 'string') ? '#' + e : e).css('display', 'block'); 620 }, 621 622 hide : function(e) { 623 if (is(e, 'array') && is(e[0], 'string')) 624 e = e.join(',#'); 625 626 $(is(e, 'string') ? '#' + e : e).css('display', 'none'); 627 }, 628 629 isHidden : function(e) { 630 return $(is(e, 'string') ? '#' + e : e).is(':hidden'); 631 }, 632 633 insertAfter : function(n, e) { 634 return $(is(e, 'string') ? '#' + e : e).after(n); 635 }, 636 637 replace : function(o, n, k) { 638 n = $(is(n, 'string') ? '#' + n : n); 639 640 if (k) 641 n.children().appendTo(o); 642 643 n.replaceWith(o); 644 }, 645 646 setStyle : function(n, na, v) { 647 if (is(n, 'array') && is(n[0], 'string')) 648 n = n.join(',#'); 649 650 $(is(n, 'string') ? '#' + n : n).css(na, v); 651 }, 652 653 getStyle : function(n, na, c) { 654 return $(is(n, 'string') ? '#' + n : n).css(na); 655 }, 656 657 setStyles : function(e, o) { 658 if (is(e, 'array') && is(e[0], 'string')) 659 e = e.join(',#'); 660 $(is(e, 'string') ? '#' + e : e).css(o); 661 }, 662 663 setAttrib : function(e, n, v) { 664 var t = this, s = t.settings; 665 666 if (is(e, 'array') && is(e[0], 'string')) 667 e = e.join(',#'); 668 669 e = $(is(e, 'string') ? '#' + e : e); 670 671 switch (n) { 672 case "style": 673 e.each(function(i, v){ 674 if (s.keep_values) 675 $(v).attr('data-mce-style', v); 676 677 v.style.cssText = v; 678 }); 679 break; 680 681 case "class": 682 e.each(function(){ 683 this.className = v; 684 }); 685 break; 686 687 case "src": 688 case "href": 689 e.each(function(i, v){ 690 if (s.keep_values) { 691 if (s.url_converter) 692 v = s.url_converter.call(s.url_converter_scope || t, v, n, v); 693 694 t.setAttrib(v, 'data-mce-' + n, v); 695 } 696 }); 697 698 break; 699 } 700 701 if (v !== null && v.length !== 0) 702 e.attr(n, '' + v); 703 else 704 e.removeAttr(n); 705 }, 706 707 setAttribs : function(e, o) { 708 var t = this; 709 710 $.each(o, function(n, v){ 711 t.setAttrib(e,n,v); 712 }); 713 } 714 */ 715 } 716 717 /* 718 'tinymce.dom.Event' : { 719 add : function (o, n, f, s) { 720 var lo, cb; 721 722 cb = function(e) { 723 e.target = e.target || this; 724 f.call(s || this, e); 725 }; 726 727 if (is(o, 'array') && is(o[0], 'string')) 728 o = o.join(',#'); 729 o = $(is(o, 'string') ? '#' + o : o); 730 if (n == 'init') { 731 o.ready(cb, s); 732 } else { 733 if (s) { 734 o.bind(n, s, cb); 735 } else { 736 o.bind(n, cb); 737 } 738 } 739 740 lo = this._jqLookup || (this._jqLookup = []); 741 lo.push({func : f, cfunc : cb}); 742 743 return cb; 744 }, 745 746 remove : function(o, n, f) { 747 // Find cfunc 748 $(this._jqLookup).each(function() { 749 if (this.func === f) 750 f = this.cfunc; 751 }); 752 753 if (is(o, 'array') && is(o[0], 'string')) 754 o = o.join(',#'); 755 756 $(is(o, 'string') ? '#' + o : o).unbind(n,f); 757 758 return true; 759 } 760 } 761 */ 762 }; 763 764 // Patch functions after a class is created 765 tinymce.onCreate = function(ty, c, p) { 766 tinymce.extend(p, patches[c]); 767 }; 768 })(window.jQuery, tinymce); 769 770 771 772 tinymce.create('tinymce.util.Dispatcher', { 773 scope : null, 774 listeners : null, 775 inDispatch: false, 776 777 Dispatcher : function(scope) { 778 this.scope = scope || this; 779 this.listeners = []; 780 }, 781 782 add : function(callback, scope) { 783 this.listeners.push({cb : callback, scope : scope || this.scope}); 784 785 return callback; 786 }, 787 788 addToTop : function(callback, scope) { 789 var self = this, listener = {cb : callback, scope : scope || self.scope}; 790 791 // Create new listeners if addToTop is executed in a dispatch loop 792 if (self.inDispatch) { 793 self.listeners = [listener].concat(self.listeners); 794 } else { 795 self.listeners.unshift(listener); 796 } 797 798 return callback; 799 }, 800 801 remove : function(callback) { 802 var listeners = this.listeners, output = null; 803 804 tinymce.each(listeners, function(listener, i) { 805 if (callback == listener.cb) { 806 output = listener; 807 listeners.splice(i, 1); 808 return false; 809 } 810 }); 811 812 return output; 813 }, 814 815 dispatch : function() { 816 var self = this, returnValue, args = arguments, i, listeners = self.listeners, listener; 817 818 self.inDispatch = true; 819 820 // Needs to be a real loop since the listener count might change while looping 821 // And this is also more efficient 822 for (i = 0; i < listeners.length; i++) { 823 listener = listeners[i]; 824 returnValue = listener.cb.apply(listener.scope, args.length > 0 ? args : [listener.scope]); 825 826 if (returnValue === false) 827 break; 828 } 829 830 self.inDispatch = false; 831 832 return returnValue; 833 } 834 835 }); 836 837 (function() { 838 var each = tinymce.each; 839 840 tinymce.create('tinymce.util.URI', { 841 URI : function(u, s) { 842 var t = this, o, a, b, base_url; 843 844 // Trim whitespace 845 u = tinymce.trim(u); 846 847 // Default settings 848 s = t.settings = s || {}; 849 850 // Strange app protocol that isn't http/https or local anchor 851 // For example: mailto,skype,tel etc. 852 if (/^([\w\-]+):([^\/]{2})/i.test(u) || /^\s*#/.test(u)) { 853 t.source = u; 854 return; 855 } 856 857 // Absolute path with no host, fake host and protocol 858 if (u.indexOf('/') === 0 && u.indexOf('//') !== 0) 859 u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u; 860 861 // Relative path http:// or protocol relative //path 862 if (!/^[\w\-]*:?\/\//.test(u)) { 863 base_url = s.base_uri ? s.base_uri.path : new tinymce.util.URI(location.href).directory; 864 u = ((s.base_uri && s.base_uri.protocol) || 'http') + '://mce_host' + t.toAbsPath(base_url, u); 865 } 866 867 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) 868 u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something 869 u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u); 870 each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) { 871 var s = u[i]; 872 873 // Zope 3 workaround, they use @@something 874 if (s) 875 s = s.replace(/\(mce_at\)/g, '@@'); 876 877 t[v] = s; 878 }); 879 880 b = s.base_uri; 881 if (b) { 882 if (!t.protocol) 883 t.protocol = b.protocol; 884 885 if (!t.userInfo) 886 t.userInfo = b.userInfo; 887 888 if (!t.port && t.host === 'mce_host') 889 t.port = b.port; 890 891 if (!t.host || t.host === 'mce_host') 892 t.host = b.host; 893 894 t.source = ''; 895 } 896 897 //t.path = t.path || '/'; 898 }, 899 900 setPath : function(p) { 901 var t = this; 902 903 p = /^(.*?)\/?(\w+)?$/.exec(p); 904 905 // Update path parts 906 t.path = p[0]; 907 t.directory = p[1]; 908 t.file = p[2]; 909 910 // Rebuild source 911 t.source = ''; 912 t.getURI(); 913 }, 914 915 toRelative : function(u) { 916 var t = this, o; 917 918 if (u === "./") 919 return u; 920 921 u = new tinymce.util.URI(u, {base_uri : t}); 922 923 // Not on same domain/port or protocol 924 if ((u.host != 'mce_host' && t.host != u.host && u.host) || t.port != u.port || t.protocol != u.protocol) 925 return u.getURI(); 926 927 var tu = t.getURI(), uu = u.getURI(); 928 929 // Allow usage of the base_uri when relative_urls = true 930 if(tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) 931 return tu; 932 933 o = t.toRelPath(t.path, u.path); 934 935 // Add query 936 if (u.query) 937 o += '?' + u.query; 938 939 // Add anchor 940 if (u.anchor) 941 o += '#' + u.anchor; 942 943 return o; 944 }, 945 946 toAbsolute : function(u, nh) { 947 u = new tinymce.util.URI(u, {base_uri : this}); 948 949 return u.getURI(this.host == u.host && this.protocol == u.protocol ? nh : 0); 950 }, 951 952 toRelPath : function(base, path) { 953 var items, bp = 0, out = '', i, l; 954 955 // Split the paths 956 base = base.substring(0, base.lastIndexOf('/')); 957 base = base.split('/'); 958 items = path.split('/'); 959 960 if (base.length >= items.length) { 961 for (i = 0, l = base.length; i < l; i++) { 962 if (i >= items.length || base[i] != items[i]) { 963 bp = i + 1; 964 break; 965 } 966 } 967 } 968 969 if (base.length < items.length) { 970 for (i = 0, l = items.length; i < l; i++) { 971 if (i >= base.length || base[i] != items[i]) { 972 bp = i + 1; 973 break; 974 } 975 } 976 } 977 978 if (bp === 1) 979 return path; 980 981 for (i = 0, l = base.length - (bp - 1); i < l; i++) 982 out += "../"; 983 984 for (i = bp - 1, l = items.length; i < l; i++) { 985 if (i != bp - 1) 986 out += "/" + items[i]; 987 else 988 out += items[i]; 989 } 990 991 return out; 992 }, 993 994 toAbsPath : function(base, path) { 995 var i, nb = 0, o = [], tr, outPath; 996 997 // Split paths 998 tr = /\/$/.test(path) ? '/' : ''; 999 base = base.split('/'); 1000 path = path.split('/'); 1001 1002 // Remove empty chunks 1003 each(base, function(k) { 1004 if (k) 1005 o.push(k); 1006 }); 1007 1008 base = o; 1009 1010 // Merge relURLParts chunks 1011 for (i = path.length - 1, o = []; i >= 0; i--) { 1012 // Ignore empty or . 1013 if (path[i].length === 0 || path[i] === ".") 1014 continue; 1015 1016 // Is parent 1017 if (path[i] === '..') { 1018 nb++; 1019 continue; 1020 } 1021 1022 // Move up 1023 if (nb > 0) { 1024 nb--; 1025 continue; 1026 } 1027 1028 o.push(path[i]); 1029 } 1030 1031 i = base.length - nb; 1032 1033 // If /a/b/c or / 1034 if (i <= 0) 1035 outPath = o.reverse().join('/'); 1036 else 1037 outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); 1038 1039 // Add front / if it's needed 1040 if (outPath.indexOf('/') !== 0) 1041 outPath = '/' + outPath; 1042 1043 // Add traling / if it's needed 1044 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) 1045 outPath += tr; 1046 1047 return outPath; 1048 }, 1049 1050 getURI : function(nh) { 1051 var s, t = this; 1052 1053 // Rebuild source 1054 if (!t.source || nh) { 1055 s = ''; 1056 1057 if (!nh) { 1058 if (t.protocol) 1059 s += t.protocol + '://'; 1060 1061 if (t.userInfo) 1062 s += t.userInfo + '@'; 1063 1064 if (t.host) 1065 s += t.host; 1066 1067 if (t.port) 1068 s += ':' + t.port; 1069 } 1070 1071 if (t.path) 1072 s += t.path; 1073 1074 if (t.query) 1075 s += '?' + t.query; 1076 1077 if (t.anchor) 1078 s += '#' + t.anchor; 1079 1080 t.source = s; 1081 } 1082 1083 return t.source; 1084 } 1085 }); 1086 })(); 1087 1088 (function() { 1089 var each = tinymce.each; 1090 1091 tinymce.create('static tinymce.util.Cookie', { 1092 getHash : function(n) { 1093 var v = this.get(n), h; 1094 1095 if (v) { 1096 each(v.split('&'), function(v) { 1097 v = v.split('='); 1098 h = h || {}; 1099 h[unescape(v[0])] = unescape(v[1]); 1100 }); 1101 } 1102 1103 return h; 1104 }, 1105 1106 setHash : function(n, v, e, p, d, s) { 1107 var o = ''; 1108 1109 each(v, function(v, k) { 1110 o += (!o ? '' : '&') + escape(k) + '=' + escape(v); 1111 }); 1112 1113 this.set(n, o, e, p, d, s); 1114 }, 1115 1116 get : function(n) { 1117 var c = document.cookie, e, p = n + "=", b; 1118 1119 // Strict mode 1120 if (!c) 1121 return; 1122 1123 b = c.indexOf("; " + p); 1124 1125 if (b == -1) { 1126 b = c.indexOf(p); 1127 1128 if (b !== 0) 1129 return null; 1130 } else 1131 b += 2; 1132 1133 e = c.indexOf(";", b); 1134 1135 if (e == -1) 1136 e = c.length; 1137 1138 return unescape(c.substring(b + p.length, e)); 1139 }, 1140 1141 set : function(n, v, e, p, d, s) { 1142 document.cookie = n + "=" + escape(v) + 1143 ((e) ? "; expires=" + e.toGMTString() : "") + 1144 ((p) ? "; path=" + escape(p) : "") + 1145 ((d) ? "; domain=" + d : "") + 1146 ((s) ? "; secure" : ""); 1147 }, 1148 1149 remove : function(name, path, domain) { 1150 var date = new Date(); 1151 1152 date.setTime(date.getTime() - 1000); 1153 1154 this.set(name, '', date, path, domain); 1155 } 1156 }); 1157 })(); 1158 1159 (function() { 1160 function serialize(o, quote) { 1161 var i, v, t, name; 1162 1163 quote = quote || '"'; 1164 1165 if (o == null) 1166 return 'null'; 1167 1168 t = typeof o; 1169 1170 if (t == 'string') { 1171 v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; 1172 1173 return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { 1174 // Make sure single quotes never get encoded inside double quotes for JSON compatibility 1175 if (quote === '"' && a === "'") 1176 return a; 1177 1178 i = v.indexOf(b); 1179 1180 if (i + 1) 1181 return '\\' + v.charAt(i + 1); 1182 1183 a = b.charCodeAt().toString(16); 1184 1185 return '\\u' + '0000'.substring(a.length) + a; 1186 }) + quote; 1187 } 1188 1189 if (t == 'object') { 1190 if (o.hasOwnProperty && o instanceof Array) { 1191 for (i=0, v = '['; i<o.length; i++) 1192 v += (i > 0 ? ',' : '') + serialize(o[i], quote); 1193 1194 return v + ']'; 1195 } 1196 1197 v = '{'; 1198 1199 for (name in o) { 1200 if (o.hasOwnProperty(name)) { 1201 v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + quote +':' + serialize(o[name], quote) : ''; 1202 } 1203 } 1204 1205 return v + '}'; 1206 } 1207 1208 return '' + o; 1209 }; 1210 1211 tinymce.util.JSON = { 1212 serialize: serialize, 1213 1214 parse: function(s) { 1215 try { 1216 return eval('(' + s + ')'); 1217 } catch (ex) { 1218 // Ignore 1219 } 1220 } 1221 1222 }; 1223 })(); 1224 1225 tinymce.create('static tinymce.util.XHR', { 1226 send : function(o) { 1227 var x, t, w = window, c = 0; 1228 1229 function ready() { 1230 if (!o.async || x.readyState == 4 || c++ > 10000) { 1231 if (o.success && c < 10000 && x.status == 200) 1232 o.success.call(o.success_scope, '' + x.responseText, x, o); 1233 else if (o.error) 1234 o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o); 1235 1236 x = null; 1237 } else 1238 w.setTimeout(ready, 10); 1239 }; 1240 1241 // Default settings 1242 o.scope = o.scope || this; 1243 o.success_scope = o.success_scope || o.scope; 1244 o.error_scope = o.error_scope || o.scope; 1245 o.async = o.async === false ? false : true; 1246 o.data = o.data || ''; 1247 1248 function get(s) { 1249 x = 0; 1250 1251 try { 1252 x = new ActiveXObject(s); 1253 } catch (ex) { 1254 } 1255 1256 return x; 1257 }; 1258 1259 x = w.XMLHttpRequest ? new XMLHttpRequest() : get('Microsoft.XMLHTTP') || get('Msxml2.XMLHTTP'); 1260 1261 if (x) { 1262 if (x.overrideMimeType) 1263 x.overrideMimeType(o.content_type); 1264 1265 x.open(o.type || (o.data ? 'POST' : 'GET'), o.url, o.async); 1266 1267 if (o.content_type) 1268 x.setRequestHeader('Content-Type', o.content_type); 1269 1270 x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 1271 1272 x.send(o.data); 1273 1274 // Syncronous request 1275 if (!o.async) 1276 return ready(); 1277 1278 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE 1279 t = w.setTimeout(ready, 10); 1280 } 1281 } 1282 }); 1283 1284 (function() { 1285 var extend = tinymce.extend, JSON = tinymce.util.JSON, XHR = tinymce.util.XHR; 1286 1287 tinymce.create('tinymce.util.JSONRequest', { 1288 JSONRequest : function(s) { 1289 this.settings = extend({ 1290 }, s); 1291 this.count = 0; 1292 }, 1293 1294 send : function(o) { 1295 var ecb = o.error, scb = o.success; 1296 1297 o = extend(this.settings, o); 1298 1299 o.success = function(c, x) { 1300 c = JSON.parse(c); 1301 1302 if (typeof(c) == 'undefined') { 1303 c = { 1304 error : 'JSON Parse error.' 1305 }; 1306 } 1307 1308 if (c.error) 1309 ecb.call(o.error_scope || o.scope, c.error, x); 1310 else 1311 scb.call(o.success_scope || o.scope, c.result); 1312 }; 1313 1314 o.error = function(ty, x) { 1315 if (ecb) 1316 ecb.call(o.error_scope || o.scope, ty, x); 1317 }; 1318 1319 o.data = JSON.serialize({ 1320 id : o.id || 'c' + (this.count++), 1321 method : o.method, 1322 params : o.params 1323 }); 1324 1325 // JSON content type for Ruby on rails. Bug: #1883287 1326 o.content_type = 'application/json'; 1327 1328 XHR.send(o); 1329 }, 1330 1331 'static' : { 1332 sendRPC : function(o) { 1333 return new tinymce.util.JSONRequest().send(o); 1334 } 1335 } 1336 }); 1337 }()); 1338 (function(tinymce){ 1339 tinymce.VK = { 1340 BACKSPACE: 8, 1341 DELETE: 46, 1342 DOWN: 40, 1343 ENTER: 13, 1344 LEFT: 37, 1345 RIGHT: 39, 1346 SPACEBAR: 32, 1347 TAB: 9, 1348 UP: 38, 1349 1350 modifierPressed: function (e) { 1351 return e.shiftKey || e.ctrlKey || e.altKey; 1352 }, 1353 1354 metaKeyPressed: function(e) { 1355 return tinymce.isMac ? e.metaKey : e.ctrlKey; 1356 } 1357 }; 1358 })(tinymce); 1359 1360 tinymce.util.Quirks = function(editor) { 1361 var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, settings = editor.settings; 1362 1363 function setEditorCommandState(cmd, state) { 1364 try { 1365 editor.getDoc().execCommand(cmd, false, state); 1366 } catch (ex) { 1367 // Ignore 1368 } 1369 } 1370 1371 function getDocumentMode() { 1372 var documentMode = editor.getDoc().documentMode; 1373 1374 return documentMode ? documentMode : 6; 1375 }; 1376 1377 function cleanupStylesWhenDeleting() { 1378 function removeMergedFormatSpans(isDelete) { 1379 var rng, blockElm, node, clonedSpan; 1380 1381 rng = selection.getRng(); 1382 1383 // Find root block 1384 blockElm = dom.getParent(rng.startContainer, dom.isBlock); 1385 1386 // On delete clone the root span of the next block element 1387 if (isDelete) 1388 blockElm = dom.getNext(blockElm, dom.isBlock); 1389 1390 // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace 1391 if (blockElm) { 1392 node = blockElm.firstChild; 1393 1394 // Ignore empty text nodes 1395 while (node && node.nodeType == 3 && node.nodeValue.length === 0) 1396 node = node.nextSibling; 1397 1398 if (node && node.nodeName === 'SPAN') { 1399 clonedSpan = node.cloneNode(false); 1400 } 1401 } 1402 1403 // Do the backspace/delete action 1404 editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); 1405 1406 // Find all odd apple-style-spans 1407 blockElm = dom.getParent(rng.startContainer, dom.isBlock); 1408 tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) { 1409 var bm = selection.getBookmark(); 1410 1411 if (clonedSpan) { 1412 dom.replace(clonedSpan.cloneNode(false), span, true); 1413 } else { 1414 dom.remove(span, true); 1415 } 1416 1417 // Restore the selection 1418 selection.moveToBookmark(bm); 1419 }); 1420 }; 1421 1422 editor.onKeyDown.add(function(editor, e) { 1423 var isDelete; 1424 1425 isDelete = e.keyCode == DELETE; 1426 if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { 1427 e.preventDefault(); 1428 removeMergedFormatSpans(isDelete); 1429 } 1430 }); 1431 1432 editor.addCommand('Delete', function() {removeMergedFormatSpans();}); 1433 }; 1434 1435 function emptyEditorWhenDeleting() { 1436 function serializeRng(rng) { 1437 var body = dom.create("body"); 1438 var contents = rng.cloneContents(); 1439 body.appendChild(contents); 1440 return selection.serializer.serialize(body, {format: 'html'}); 1441 } 1442 1443 function allContentsSelected(rng) { 1444 var selection = serializeRng(rng); 1445 1446 var allRng = dom.createRng(); 1447 allRng.selectNode(editor.getBody()); 1448 1449 var allSelection = serializeRng(allRng);//console.log(selection, "----", allSelection); 1450 return selection === allSelection; 1451 } 1452 1453 editor.onKeyDown.add(function(editor, e) { 1454 var keyCode = e.keyCode, isCollapsed; 1455 1456 // Empty the editor if it's needed for example backspace at <p><b>|</b></p> 1457 if (!e.isDefaultPrevented() && (keyCode == DELETE || keyCode == BACKSPACE)) { 1458 isCollapsed = editor.selection.isCollapsed(); 1459 1460 // Selection is collapsed but the editor isn't empty 1461 if (isCollapsed && !dom.isEmpty(editor.getBody())) { 1462 return; 1463 } 1464 1465 // IE deletes all contents correctly when everything is selected 1466 if (tinymce.isIE && !isCollapsed) { 1467 return; 1468 } 1469 1470 // Selection isn't collapsed but not all the contents is selected 1471 if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { 1472 return; 1473 } 1474 1475 // Manually empty the editor 1476 editor.setContent(''); 1477 editor.selection.setCursorLocation(editor.getBody(), 0); 1478 editor.nodeChanged(); 1479 } 1480 }); 1481 }; 1482 1483 function selectAll() { 1484 editor.onKeyDown.add(function(editor, e) { 1485 if (e.keyCode == 65 && VK.metaKeyPressed(e)) { 1486 e.preventDefault(); 1487 editor.execCommand('SelectAll'); 1488 } 1489 }); 1490 }; 1491 1492 function inputMethodFocus() { 1493 if (!editor.settings.content_editable) { 1494 // Case 1 IME doesn't initialize if you focus the document 1495 dom.bind(editor.getDoc(), 'focusin', function(e) { 1496 selection.setRng(selection.getRng()); 1497 }); 1498 1499 // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event 1500 dom.bind(editor.getDoc(), 'mousedown', function(e) { 1501 if (e.target == editor.getDoc().documentElement) { 1502 editor.getWin().focus(); 1503 selection.setRng(selection.getRng()); 1504 } 1505 }); 1506 } 1507 }; 1508 1509 function removeHrOnBackspace() { 1510 editor.onKeyDown.add(function(editor, e) { 1511 if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) { 1512 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 1513 var node = selection.getNode(); 1514 var previousSibling = node.previousSibling; 1515 1516 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { 1517 dom.remove(previousSibling); 1518 tinymce.dom.Event.cancel(e); 1519 } 1520 } 1521 } 1522 }) 1523 } 1524 1525 function focusBody() { 1526 // Fix for a focus bug in FF 3.x where the body element 1527 // wouldn't get proper focus if the user clicked on the HTML element 1528 if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 1529 editor.onMouseDown.add(function(editor, e) { 1530 if (e.target.nodeName === "HTML") { 1531 var body = editor.getBody(); 1532 1533 // Blur the body it's focused but not correctly focused 1534 body.blur(); 1535 1536 // Refocus the body after a little while 1537 setTimeout(function() { 1538 body.focus(); 1539 }, 0); 1540 } 1541 }); 1542 } 1543 }; 1544 1545 function selectControlElements() { 1546 editor.onClick.add(function(editor, e) { 1547 e = e.target; 1548 1549 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 1550 // WebKit can't even do simple things like selecting an image 1551 // Needs tobe the setBaseAndExtend or it will fail to select floated images 1552 if (/^(IMG|HR)$/.test(e.nodeName)) { 1553 selection.getSel().setBaseAndExtent(e, 0, e, 1); 1554 } 1555 1556 if (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor')) { 1557 selection.select(e); 1558 } 1559 1560 editor.nodeChanged(); 1561 }); 1562 }; 1563 1564 function removeStylesWhenDeletingAccrossBlockElements() { 1565 function getAttributeApplyFunction() { 1566 var template = dom.getAttribs(selection.getStart().cloneNode(false)); 1567 1568 return function() { 1569 var target = selection.getStart(); 1570 1571 if (target !== editor.getBody()) { 1572 dom.setAttrib(target, "style", null); 1573 1574 tinymce.each(template, function(attr) { 1575 target.setAttributeNode(attr.cloneNode(true)); 1576 }); 1577 } 1578 }; 1579 } 1580 1581 function isSelectionAcrossElements() { 1582 return !selection.isCollapsed() && selection.getStart() != selection.getEnd(); 1583 } 1584 1585 function blockEvent(editor, e) { 1586 e.preventDefault(); 1587 return false; 1588 } 1589 1590 editor.onKeyPress.add(function(editor, e) { 1591 var applyAttributes; 1592 1593 if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { 1594 applyAttributes = getAttributeApplyFunction(); 1595 editor.getDoc().execCommand('delete', false, null); 1596 applyAttributes(); 1597 e.preventDefault(); 1598 return false; 1599 } 1600 }); 1601 1602 dom.bind(editor.getDoc(), 'cut', function(e) { 1603 var applyAttributes; 1604 1605 if (isSelectionAcrossElements()) { 1606 applyAttributes = getAttributeApplyFunction(); 1607 editor.onKeyUp.addToTop(blockEvent); 1608 1609 setTimeout(function() { 1610 applyAttributes(); 1611 editor.onKeyUp.remove(blockEvent); 1612 }, 0); 1613 } 1614 }); 1615 } 1616 1617 function selectionChangeNodeChanged() { 1618 var lastRng, selectionTimer; 1619 1620 dom.bind(editor.getDoc(), 'selectionchange', function() { 1621 if (selectionTimer) { 1622 clearTimeout(selectionTimer); 1623 selectionTimer = 0; 1624 } 1625 1626 selectionTimer = window.setTimeout(function() { 1627 var rng = selection.getRng(); 1628 1629 // Compare the ranges to see if it was a real change or not 1630 if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) { 1631 editor.nodeChanged(); 1632 lastRng = rng; 1633 } 1634 }, 50); 1635 }); 1636 } 1637 1638 function ensureBodyHasRoleApplication() { 1639 document.body.setAttribute("role", "application"); 1640 } 1641 1642 function disableBackspaceIntoATable() { 1643 editor.onKeyDown.add(function(editor, e) { 1644 if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) { 1645 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 1646 var previousSibling = selection.getNode().previousSibling; 1647 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { 1648 return tinymce.dom.Event.cancel(e); 1649 } 1650 } 1651 } 1652 }) 1653 } 1654 1655 function addNewLinesBeforeBrInPre() { 1656 // IE8+ rendering mode does the right thing with BR in PRE 1657 if (getDocumentMode() > 7) { 1658 return; 1659 } 1660 1661 // Enable display: none in area and add a specific class that hides all BR elements in PRE to 1662 // avoid the caret from getting stuck at the BR elements while pressing the right arrow key 1663 setEditorCommandState('RespectVisibilityInDesign', true); 1664 editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); 1665 dom.addClass(editor.getBody(), 'mceHideBrInPre'); 1666 1667 // Adds a \n before all BR elements in PRE to get them visual 1668 editor.parser.addNodeFilter('pre', function(nodes, name) { 1669 var i = nodes.length, brNodes, j, brElm, sibling; 1670 1671 while (i--) { 1672 brNodes = nodes[i].getAll('br'); 1673 j = brNodes.length; 1674 while (j--) { 1675 brElm = brNodes[j]; 1676 1677 // Add \n before BR in PRE elements on older IE:s so the new lines get rendered 1678 sibling = brElm.prev; 1679 if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { 1680 sibling.value += '\n'; 1681 } else { 1682 brElm.parent.insert(new tinymce.html.Node('#text', 3), brElm, true).value = '\n'; 1683 } 1684 } 1685 } 1686 }); 1687 1688 // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible 1689 editor.serializer.addNodeFilter('pre', function(nodes, name) { 1690 var i = nodes.length, brNodes, j, brElm, sibling; 1691 1692 while (i--) { 1693 brNodes = nodes[i].getAll('br'); 1694 j = brNodes.length; 1695 while (j--) { 1696 brElm = brNodes[j]; 1697 sibling = brElm.prev; 1698 if (sibling && sibling.type == 3) { 1699 sibling.value = sibling.value.replace(/\r?\n$/, ''); 1700 } 1701 } 1702 } 1703 }); 1704 } 1705 1706 function removePreSerializedStylesWhenSelectingControls() { 1707 dom.bind(editor.getBody(), 'mouseup', function(e) { 1708 var value, node = selection.getNode(); 1709 1710 // Moved styles to attributes on IMG eements 1711 if (node.nodeName == 'IMG') { 1712 // Convert style width to width attribute 1713 if (value = dom.getStyle(node, 'width')) { 1714 dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); 1715 dom.setStyle(node, 'width', ''); 1716 } 1717 1718 // Convert style height to height attribute 1719 if (value = dom.getStyle(node, 'height')) { 1720 dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); 1721 dom.setStyle(node, 'height', ''); 1722 } 1723 } 1724 }); 1725 } 1726 1727 function keepInlineElementOnDeleteBackspace() { 1728 editor.onKeyDown.add(function(editor, e) { 1729 var isDelete, rng, container, offset, brElm, sibling, collapsed; 1730 1731 isDelete = e.keyCode == DELETE; 1732 if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { 1733 rng = selection.getRng(); 1734 container = rng.startContainer; 1735 offset = rng.startOffset; 1736 collapsed = rng.collapsed; 1737 1738 // Override delete if the start container is a text node and is at the beginning of text or 1739 // just before/after the last character to be deleted in collapsed mode 1740 if (container.nodeType == 3 && container.nodeValue.length > 0 && ((offset === 0 && !collapsed) || (collapsed && offset === (isDelete ? 0 : 1)))) { 1741 nonEmptyElements = editor.schema.getNonEmptyElements(); 1742 1743 // Prevent default logic since it's broken 1744 e.preventDefault(); 1745 1746 // Insert a BR before the text node this will prevent the containing element from being deleted/converted 1747 brElm = dom.create('br', {id: '__tmp'}); 1748 container.parentNode.insertBefore(brElm, container); 1749 1750 // Do the browser delete 1751 editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); 1752 1753 // Check if the previous sibling is empty after deleting for example: <p><b></b>|</p> 1754 container = selection.getRng().startContainer; 1755 sibling = container.previousSibling; 1756 if (sibling && sibling.nodeType == 1 && !dom.isBlock(sibling) && dom.isEmpty(sibling) && !nonEmptyElements[sibling.nodeName.toLowerCase()]) { 1757 dom.remove(sibling); 1758 } 1759 1760 // Remove the temp element we inserted 1761 dom.remove('__tmp'); 1762 } 1763 } 1764 }); 1765 } 1766 1767 function removeBlockQuoteOnBackSpace() { 1768 // Add block quote deletion handler 1769 editor.onKeyDown.add(function(editor, e) { 1770 var rng, container, offset, root, parent; 1771 1772 if (e.isDefaultPrevented() || e.keyCode != VK.BACKSPACE) { 1773 return; 1774 } 1775 1776 rng = selection.getRng(); 1777 container = rng.startContainer; 1778 offset = rng.startOffset; 1779 root = dom.getRoot(); 1780 parent = container; 1781 1782 if (!rng.collapsed || offset !== 0) { 1783 return; 1784 } 1785 1786 while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { 1787 parent = parent.parentNode; 1788 } 1789 1790 // Is the cursor at the beginning of a blockquote? 1791 if (parent.tagName === 'BLOCKQUOTE') { 1792 // Remove the blockquote 1793 editor.formatter.toggle('blockquote', null, parent); 1794 1795 // Move the caret to the beginning of container 1796 rng.setStart(container, 0); 1797 rng.setEnd(container, 0); 1798 selection.setRng(rng); 1799 selection.collapse(false); 1800 } 1801 }); 1802 }; 1803 1804 function setGeckoEditingOptions() { 1805 function setOpts() { 1806 editor._refreshContentEditable(); 1807 1808 setEditorCommandState("StyleWithCSS", false); 1809 setEditorCommandState("enableInlineTableEditing", false); 1810 1811 if (!settings.object_resizing) { 1812 setEditorCommandState("enableObjectResizing", false); 1813 } 1814 }; 1815 1816 if (!settings.readonly) { 1817 editor.onBeforeExecCommand.add(setOpts); 1818 editor.onMouseDown.add(setOpts); 1819 } 1820 }; 1821 1822 function addBrAfterLastLinks() { 1823 function fixLinks(editor, o) { 1824 tinymce.each(dom.select('a'), function(node) { 1825 var parentNode = node.parentNode, root = dom.getRoot(); 1826 1827 if (parentNode.lastChild === node) { 1828 while (parentNode && !dom.isBlock(parentNode)) { 1829 if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { 1830 return; 1831 } 1832 1833 parentNode = parentNode.parentNode; 1834 } 1835 1836 dom.add(parentNode, 'br', {'data-mce-bogus' : 1}); 1837 } 1838 }); 1839 }; 1840 1841 editor.onExecCommand.add(function(editor, cmd) { 1842 if (cmd === 'CreateLink') { 1843 fixLinks(editor); 1844 } 1845 }); 1846 1847 editor.onSetContent.add(selection.onSetContent.add(fixLinks)); 1848 }; 1849 1850 function setDefaultBlockType() { 1851 if (settings.forced_root_block) { 1852 editor.onInit.add(function() { 1853 setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); 1854 }); 1855 } 1856 } 1857 1858 function removeGhostSelection() { 1859 function repaint(sender, args) { 1860 if (!sender || !args.initial) { 1861 editor.execCommand('mceRepaint'); 1862 } 1863 }; 1864 1865 editor.onUndo.add(repaint); 1866 editor.onRedo.add(repaint); 1867 editor.onSetContent.add(repaint); 1868 }; 1869 1870 function deleteControlItemOnBackSpace() { 1871 editor.onKeyDown.add(function(editor, e) { 1872 var rng; 1873 1874 if (!e.isDefaultPrevented() && e.keyCode == BACKSPACE) { 1875 rng = editor.getDoc().selection.createRange(); 1876 if (rng && rng.item) { 1877 e.preventDefault(); 1878 editor.undoManager.beforeChange(); 1879 dom.remove(rng.item(0)); 1880 editor.undoManager.add(); 1881 } 1882 } 1883 }); 1884 }; 1885 1886 function renderEmptyBlocksFix() { 1887 var emptyBlocksCSS; 1888 1889 // IE10+ 1890 if (getDocumentMode() >= 10) { 1891 emptyBlocksCSS = ''; 1892 tinymce.each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { 1893 emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; 1894 }); 1895 1896 editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); 1897 } 1898 }; 1899 1900 function fakeImageResize() { 1901 var mouseDownImg, startX, startY, startW, startH; 1902 1903 if (!settings.object_resizing || settings.webkit_fake_resize === false) { 1904 return; 1905 } 1906 1907 editor.contentStyles.push('.mceResizeImages img {cursor: se-resize !important}'); 1908 1909 function resizeImage(e) { 1910 var deltaX, deltaY, ratio, width, height; 1911 1912 if (mouseDownImg) { 1913 deltaX = e.screenX - startX; 1914 deltaY = e.screenY - startY; 1915 ratio = Math.max((startW + deltaX) / startW, (startH + deltaY) / startH); 1916 1917 // Only update styles if the user draged one pixel or more 1918 if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { 1919 // Constrain proportions 1920 width = Math.round(startW * ratio); 1921 height = Math.round(startH * ratio); 1922 1923 // Resize by using style or attribute 1924 if (mouseDownImg.style.width) { 1925 dom.setStyle(mouseDownImg, 'width', width); 1926 } else { 1927 dom.setAttrib(mouseDownImg, 'width', width); 1928 } 1929 1930 // Resize by using style or attribute 1931 if (mouseDownImg.style.height) { 1932 dom.setStyle(mouseDownImg, 'height', height); 1933 } else { 1934 dom.setAttrib(mouseDownImg, 'height', height); 1935 } 1936 1937 if (!dom.hasClass(editor.getBody(), 'mceResizeImages')) { 1938 dom.addClass(editor.getBody(), 'mceResizeImages'); 1939 } 1940 } 1941 } 1942 }; 1943 1944 editor.onMouseDown.add(function(editor, e) { 1945 var target = e.target; 1946 1947 if (target.nodeName == "IMG") { 1948 mouseDownImg = target; 1949 startX = e.screenX; 1950 startY = e.screenY; 1951 startW = mouseDownImg.clientWidth; 1952 startH = mouseDownImg.clientHeight; 1953 dom.bind(editor.getDoc(), 'mousemove', resizeImage); 1954 e.preventDefault(); 1955 } 1956 }); 1957 1958 // Unbind events on node change and restore resize cursor 1959 editor.onNodeChange.add(function() { 1960 if (mouseDownImg) { 1961 mouseDownImg = null; 1962 dom.unbind(editor.getDoc(), 'mousemove', resizeImage); 1963 } 1964 1965 if (selection.getNode().nodeName == "IMG") { 1966 dom.addClass(editor.getBody(), 'mceResizeImages'); 1967 } else { 1968 dom.removeClass(editor.getBody(), 'mceResizeImages'); 1969 } 1970 }); 1971 }; 1972 1973 // All browsers 1974 disableBackspaceIntoATable(); 1975 removeBlockQuoteOnBackSpace(); 1976 emptyEditorWhenDeleting(); 1977 1978 // WebKit 1979 if (tinymce.isWebKit) { 1980 keepInlineElementOnDeleteBackspace(); 1981 cleanupStylesWhenDeleting(); 1982 inputMethodFocus(); 1983 selectControlElements(); 1984 setDefaultBlockType(); 1985 1986 // iOS 1987 if (tinymce.isIDevice) { 1988 selectionChangeNodeChanged(); 1989 } else { 1990 fakeImageResize(); 1991 selectAll(); 1992 } 1993 } 1994 1995 // IE 1996 if (tinymce.isIE) { 1997 removeHrOnBackspace(); 1998 ensureBodyHasRoleApplication(); 1999 addNewLinesBeforeBrInPre(); 2000 removePreSerializedStylesWhenSelectingControls(); 2001 deleteControlItemOnBackSpace(); 2002 renderEmptyBlocksFix(); 2003 } 2004 2005 // Gecko 2006 if (tinymce.isGecko) { 2007 removeHrOnBackspace(); 2008 focusBody(); 2009 removeStylesWhenDeletingAccrossBlockElements(); 2010 setGeckoEditingOptions(); 2011 addBrAfterLastLinks(); 2012 removeGhostSelection(); 2013 } 2014 }; 2015 (function(tinymce) { 2016 var namedEntities, baseEntities, reverseEntities, 2017 attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 2018 textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 2019 rawCharsRegExp = /[<>&\"\']/g, 2020 entityRegExp = /&(#x|#)?([\w]+);/g, 2021 asciiMap = { 2022 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020", 2023 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152", 2024 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022", 2025 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A", 2026 156 : "\u0153", 158 : "\u017E", 159 : "\u0178" 2027 }; 2028 2029 // Raw entities 2030 baseEntities = { 2031 '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code 2032 "'" : ''', 2033 '<' : '<', 2034 '>' : '>', 2035 '&' : '&' 2036 }; 2037 2038 // Reverse lookup table for raw entities 2039 reverseEntities = { 2040 '<' : '<', 2041 '>' : '>', 2042 '&' : '&', 2043 '"' : '"', 2044 ''' : "'" 2045 }; 2046 2047 // Decodes text by using the browser 2048 function nativeDecode(text) { 2049 var elm; 2050 2051 elm = document.createElement("div"); 2052 elm.innerHTML = text; 2053 2054 return elm.textContent || elm.innerText || text; 2055 }; 2056 2057 // Build a two way lookup table for the entities 2058 function buildEntitiesLookup(items, radix) { 2059 var i, chr, entity, lookup = {}; 2060 2061 if (items) { 2062 items = items.split(','); 2063 radix = radix || 10; 2064 2065 // Build entities lookup table 2066 for (i = 0; i < items.length; i += 2) { 2067 chr = String.fromCharCode(parseInt(items[i], radix)); 2068 2069 // Only add non base entities 2070 if (!baseEntities[chr]) { 2071 entity = '&' + items[i + 1] + ';'; 2072 lookup[chr] = entity; 2073 lookup[entity] = chr; 2074 } 2075 } 2076 2077 return lookup; 2078 } 2079 }; 2080 2081 // Unpack entities lookup where the numbers are in radix 32 to reduce the size 2082 namedEntities = buildEntitiesLookup( 2083 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + 2084 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + 2085 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + 2086 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + 2087 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + 2088 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + 2089 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + 2090 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + 2091 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + 2092 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + 2093 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + 2094 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + 2095 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + 2096 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + 2097 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + 2098 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + 2099 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + 2100 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + 2101 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + 2102 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + 2103 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + 2104 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 2105 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + 2106 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + 2107 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); 2108 2109 tinymce.html = tinymce.html || {}; 2110 2111 tinymce.html.Entities = { 2112 encodeRaw : function(text, attr) { 2113 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2114 return baseEntities[chr] || chr; 2115 }); 2116 }, 2117 2118 encodeAllRaw : function(text) { 2119 return ('' + text).replace(rawCharsRegExp, function(chr) { 2120 return baseEntities[chr] || chr; 2121 }); 2122 }, 2123 2124 encodeNumeric : function(text, attr) { 2125 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2126 // Multi byte sequence convert it to a single entity 2127 if (chr.length > 1) 2128 return '' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; 2129 2130 return baseEntities[chr] || '' + chr.charCodeAt(0) + ';'; 2131 }); 2132 }, 2133 2134 encodeNamed : function(text, attr, entities) { 2135 entities = entities || namedEntities; 2136 2137 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2138 return baseEntities[chr] || entities[chr] || chr; 2139 }); 2140 }, 2141 2142 getEncodeFunc : function(name, entities) { 2143 var Entities = tinymce.html.Entities; 2144 2145 entities = buildEntitiesLookup(entities) || namedEntities; 2146 2147 function encodeNamedAndNumeric(text, attr) { 2148 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2149 return baseEntities[chr] || entities[chr] || '' + chr.charCodeAt(0) + ';' || chr; 2150 }); 2151 }; 2152 2153 function encodeCustomNamed(text, attr) { 2154 return Entities.encodeNamed(text, attr, entities); 2155 }; 2156 2157 // Replace + with , to be compatible with previous TinyMCE versions 2158 name = tinymce.makeMap(name.replace(/\+/g, ',')); 2159 2160 // Named and numeric encoder 2161 if (name.named && name.numeric) 2162 return encodeNamedAndNumeric; 2163 2164 // Named encoder 2165 if (name.named) { 2166 // Custom names 2167 if (entities) 2168 return encodeCustomNamed; 2169 2170 return Entities.encodeNamed; 2171 } 2172 2173 // Numeric 2174 if (name.numeric) 2175 return Entities.encodeNumeric; 2176 2177 // Raw encoder 2178 return Entities.encodeRaw; 2179 }, 2180 2181 decode : function(text) { 2182 return text.replace(entityRegExp, function(all, numeric, value) { 2183 if (numeric) { 2184 value = parseInt(value, numeric.length === 2 ? 16 : 10); 2185 2186 // Support upper UTF 2187 if (value > 0xFFFF) { 2188 value -= 0x10000; 2189 2190 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); 2191 } else 2192 return asciiMap[value] || String.fromCharCode(value); 2193 } 2194 2195 return reverseEntities[all] || namedEntities[all] || nativeDecode(all); 2196 }); 2197 } 2198 }; 2199 })(tinymce); 2200 2201 tinymce.html.Styles = function(settings, schema) { 2202 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, 2203 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, 2204 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, 2205 trimRightRegExp = /\s+$/, 2206 urlColorRegExp = /rgb/, 2207 undef, i, encodingLookup = {}, encodingItems; 2208 2209 settings = settings || {}; 2210 2211 encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' '); 2212 for (i = 0; i < encodingItems.length; i++) { 2213 encodingLookup[encodingItems[i]] = '\uFEFF' + i; 2214 encodingLookup['\uFEFF' + i] = encodingItems[i]; 2215 } 2216 2217 function toHex(match, r, g, b) { 2218 function hex(val) { 2219 val = parseInt(val).toString(16); 2220 2221 return val.length > 1 ? val : '0' + val; // 0 -> 00 2222 }; 2223 2224 return '#' + hex(r) + hex(g) + hex(b); 2225 }; 2226 2227 return { 2228 toHex : function(color) { 2229 return color.replace(rgbRegExp, toHex); 2230 }, 2231 2232 parse : function(css) { 2233 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this; 2234 2235 function compress(prefix, suffix) { 2236 var top, right, bottom, left; 2237 2238 // Get values and check it it needs compressing 2239 top = styles[prefix + '-top' + suffix]; 2240 if (!top) 2241 return; 2242 2243 right = styles[prefix + '-right' + suffix]; 2244 if (top != right) 2245 return; 2246 2247 bottom = styles[prefix + '-bottom' + suffix]; 2248 if (right != bottom) 2249 return; 2250 2251 left = styles[prefix + '-left' + suffix]; 2252 if (bottom != left) 2253 return; 2254 2255 // Compress 2256 styles[prefix + suffix] = left; 2257 delete styles[prefix + '-top' + suffix]; 2258 delete styles[prefix + '-right' + suffix]; 2259 delete styles[prefix + '-bottom' + suffix]; 2260 delete styles[prefix + '-left' + suffix]; 2261 }; 2262 2263 function canCompress(key) { 2264 var value = styles[key], i; 2265 2266 if (!value || value.indexOf(' ') < 0) 2267 return; 2268 2269 value = value.split(' '); 2270 i = value.length; 2271 while (i--) { 2272 if (value[i] !== value[0]) 2273 return false; 2274 } 2275 2276 styles[key] = value[0]; 2277 2278 return true; 2279 }; 2280 2281 function compress2(target, a, b, c) { 2282 if (!canCompress(a)) 2283 return; 2284 2285 if (!canCompress(b)) 2286 return; 2287 2288 if (!canCompress(c)) 2289 return; 2290 2291 // Compress 2292 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; 2293 delete styles[a]; 2294 delete styles[b]; 2295 delete styles[c]; 2296 }; 2297 2298 // Encodes the specified string by replacing all \" \' ; : with _<num> 2299 function encode(str) { 2300 isEncoded = true; 2301 2302 return encodingLookup[str]; 2303 }; 2304 2305 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc 2306 // It will also decode the \" \' if keep_slashes is set to fale or omitted 2307 function decode(str, keep_slashes) { 2308 if (isEncoded) { 2309 str = str.replace(/\uFEFF[0-9]/g, function(str) { 2310 return encodingLookup[str]; 2311 }); 2312 } 2313 2314 if (!keep_slashes) 2315 str = str.replace(/\\([\'\";:])/g, "$1"); 2316 2317 return str; 2318 }; 2319 2320 function processUrl(match, url, url2, url3, str, str2) { 2321 str = str || str2; 2322 2323 if (str) { 2324 str = decode(str); 2325 2326 // Force strings into single quote format 2327 return "'" + str.replace(/\'/g, "\\'") + "'"; 2328 } 2329 2330 url = decode(url || url2 || url3); 2331 2332 // Convert the URL to relative/absolute depending on config 2333 if (urlConverter) 2334 url = urlConverter.call(urlConverterScope, url, 'style'); 2335 2336 // Output new URL format 2337 return "url('" + url.replace(/\'/g, "\\'") + "')"; 2338 }; 2339 2340 if (css) { 2341 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing 2342 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { 2343 return str.replace(/[;:]/g, encode); 2344 }); 2345 2346 // Parse styles 2347 while (matches = styleRegExp.exec(css)) { 2348 name = matches[1].replace(trimRightRegExp, '').toLowerCase(); 2349 value = matches[2].replace(trimRightRegExp, ''); 2350 2351 if (name && value.length > 0) { 2352 // Opera will produce 700 instead of bold in their style values 2353 if (name === 'font-weight' && value === '700') 2354 value = 'bold'; 2355 else if (name === 'color' || name === 'background-color') // Lowercase colors like RED 2356 value = value.toLowerCase(); 2357 2358 // Convert RGB colors to HEX 2359 value = value.replace(rgbRegExp, toHex); 2360 2361 // Convert URLs and force them into url('value') format 2362 value = value.replace(urlOrStrRegExp, processUrl); 2363 styles[name] = isEncoded ? decode(value, true) : value; 2364 } 2365 2366 styleRegExp.lastIndex = matches.index + matches[0].length; 2367 } 2368 2369 // Compress the styles to reduce it's size for example IE will expand styles 2370 compress("border", ""); 2371 compress("border", "-width"); 2372 compress("border", "-color"); 2373 compress("border", "-style"); 2374 compress("padding", ""); 2375 compress("margin", ""); 2376 compress2('border', 'border-width', 'border-style', 'border-color'); 2377 2378 // Remove pointless border, IE produces these 2379 if (styles.border === 'medium none') 2380 delete styles.border; 2381 } 2382 2383 return styles; 2384 }, 2385 2386 serialize : function(styles, element_name) { 2387 var css = '', name, value; 2388 2389 function serializeStyles(name) { 2390 var styleList, i, l, value; 2391 2392 styleList = schema.styles[name]; 2393 if (styleList) { 2394 for (i = 0, l = styleList.length; i < l; i++) { 2395 name = styleList[i]; 2396 value = styles[name]; 2397 2398 if (value !== undef && value.length > 0) 2399 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 2400 } 2401 } 2402 }; 2403 2404 // Serialize styles according to schema 2405 if (element_name && schema && schema.styles) { 2406 // Serialize global styles and element specific styles 2407 serializeStyles('*'); 2408 serializeStyles(element_name); 2409 } else { 2410 // Output the styles in the order they are inside the object 2411 for (name in styles) { 2412 value = styles[name]; 2413 2414 if (value !== undef && value.length > 0) 2415 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 2416 } 2417 } 2418 2419 return css; 2420 } 2421 }; 2422 }; 2423 2424 (function(tinymce) { 2425 var mapCache = {}, makeMap = tinymce.makeMap, each = tinymce.each; 2426 2427 function split(str, delim) { 2428 return str.split(delim || ','); 2429 }; 2430 2431 function unpack(lookup, data) { 2432 var key, elements = {}; 2433 2434 function replace(value) { 2435 return value.replace(/[A-Z]+/g, function(key) { 2436 return replace(lookup[key]); 2437 }); 2438 }; 2439 2440 // Unpack lookup 2441 for (key in lookup) { 2442 if (lookup.hasOwnProperty(key)) 2443 lookup[key] = replace(lookup[key]); 2444 } 2445 2446 // Unpack and parse data into object map 2447 replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) { 2448 attributes = split(attributes, '|'); 2449 2450 elements[name] = { 2451 attributes : makeMap(attributes), 2452 attributesOrder : attributes, 2453 children : makeMap(children, '|', {'#comment' : {}}) 2454 } 2455 }); 2456 2457 return elements; 2458 }; 2459 2460 function getHTML5() { 2461 var html5 = mapCache.html5; 2462 2463 if (!html5) { 2464 html5 = mapCache.html5 = unpack({ 2465 A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', 2466 B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|' + 2467 'meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video|wbr', 2468 C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|' + 2469 'figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|' + 2470 'p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video' 2471 }, 'html[A|manifest][body|head]' + 2472 'head[A][base|command|link|meta|noscript|script|style|title]' + 2473 'title[A][#]' + 2474 'base[A|href|target][]' + 2475 'link[A|href|rel|media|type|sizes][]' + 2476 'meta[A|http-equiv|name|content|charset][]' + 2477 'style[A|type|media|scoped][#]' + 2478 'script[A|charset|type|src|defer|async][#]' + 2479 'noscript[A][C]' + 2480 'body[A][C]' + 2481 'section[A][C]' + 2482 'nav[A][C]' + 2483 'article[A][C]' + 2484 'aside[A][C]' + 2485 'h1[A][B]' + 2486 'h2[A][B]' + 2487 'h3[A][B]' + 2488 'h4[A][B]' + 2489 'h5[A][B]' + 2490 'h6[A][B]' + 2491 'hgroup[A][h1|h2|h3|h4|h5|h6]' + 2492 'header[A][C]' + 2493 'footer[A][C]' + 2494 'address[A][C]' + 2495 'p[A][B]' + 2496 'br[A][]' + 2497 'pre[A][B]' + 2498 'dialog[A][dd|dt]' + 2499 'blockquote[A|cite][C]' + 2500 'ol[A|start|reversed][li]' + 2501 'ul[A][li]' + 2502 'li[A|value][C]' + 2503 'dl[A][dd|dt]' + 2504 'dt[A][B]' + 2505 'dd[A][C]' + 2506 'a[A|href|target|ping|rel|media|type][B]' + 2507 'em[A][B]' + 2508 'strong[A][B]' + 2509 'small[A][B]' + 2510 'cite[A][B]' + 2511 'q[A|cite][B]' + 2512 'dfn[A][B]' + 2513 'abbr[A][B]' + 2514 'code[A][B]' + 2515 'var[A][B]' + 2516 'samp[A][B]' + 2517 'kbd[A][B]' + 2518 'sub[A][B]' + 2519 'sup[A][B]' + 2520 'i[A][B]' + 2521 'b[A][B]' + 2522 'mark[A][B]' + 2523 'progress[A|value|max][B]' + 2524 'meter[A|value|min|max|low|high|optimum][B]' + 2525 'time[A|datetime][B]' + 2526 'ruby[A][B|rt|rp]' + 2527 'rt[A][B]' + 2528 'rp[A][B]' + 2529 'bdo[A][B]' + 2530 'span[A][B]' + 2531 'ins[A|cite|datetime][B]' + 2532 'del[A|cite|datetime][B]' + 2533 'figure[A][C|legend|figcaption]' + 2534 'figcaption[A][C]' + 2535 'img[A|alt|src|height|width|usemap|ismap][]' + 2536 'iframe[A|name|src|height|width|sandbox|seamless][]' + 2537 'embed[A|src|height|width|type][]' + 2538 'object[A|data|type|height|width|usemap|name|form|classid][param]' + 2539 'param[A|name|value][]' + 2540 'details[A|open][C|legend]' + 2541 'command[A|type|label|icon|disabled|checked|radiogroup][]' + 2542 'menu[A|type|label][C|li]' + 2543 'legend[A][C|B]' + 2544 'div[A][C]' + 2545 'source[A|src|type|media][]' + 2546 'audio[A|src|autobuffer|autoplay|loop|controls][source]' + 2547 'video[A|src|autobuffer|autoplay|loop|controls|width|height|poster][source]' + 2548 'hr[A][]' + 2549 'form[A|accept-charset|action|autocomplete|enctype|method|name|novalidate|target][C]' + 2550 'fieldset[A|disabled|form|name][C|legend]' + 2551 'label[A|form|for][B]' + 2552 'input[A|type|accept|alt|autocomplete|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' + 2553 'multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value|name][]' + 2554 'button[A|autofocus|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|name|value|type][B]' + 2555 'select[A|autofocus|disabled|form|multiple|name|size][option|optgroup]' + 2556 'datalist[A][B|option]' + 2557 'optgroup[A|disabled|label][option]' + 2558 'option[A|disabled|selected|label|value][]' + 2559 'textarea[A|autofocus|disabled|form|maxlength|name|placeholder|readonly|required|rows|cols|wrap][]' + 2560 'keygen[A|autofocus|challenge|disabled|form|keytype|name][]' + 2561 'output[A|for|form|name][B]' + 2562 'canvas[A|width|height][]' + 2563 'map[A|name][B|C]' + 2564 'area[A|shape|coords|href|alt|target|media|rel|ping|type][]' + 2565 'mathml[A][]' + 2566 'svg[A][]' + 2567 'table[A|border][caption|colgroup|thead|tfoot|tbody|tr]' + 2568 'caption[A][C]' + 2569 'colgroup[A|span][col]' + 2570 'col[A|span][]' + 2571 'thead[A][tr]' + 2572 'tfoot[A][tr]' + 2573 'tbody[A][tr]' + 2574 'tr[A][th|td]' + 2575 'th[A|headers|rowspan|colspan|scope][B]' + 2576 'td[A|headers|rowspan|colspan][C]' + 2577 'wbr[A][]' 2578 ); 2579 } 2580 2581 return html5; 2582 }; 2583 2584 function getHTML4() { 2585 var html4 = mapCache.html4; 2586 2587 if (!html4) { 2588 // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size 2589 html4 = mapCache.html4 = unpack({ 2590 Z : 'H|K|N|O|P', 2591 Y : 'X|form|R|Q', 2592 ZG : 'E|span|width|align|char|charoff|valign', 2593 X : 'p|T|div|U|W|isindex|fieldset|table', 2594 ZF : 'E|align|char|charoff|valign', 2595 W : 'pre|hr|blockquote|address|center|noframes', 2596 ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height', 2597 ZD : '[E][S]', 2598 U : 'ul|ol|dl|menu|dir', 2599 ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', 2600 T : 'h1|h2|h3|h4|h5|h6', 2601 ZB : 'X|S|Q', 2602 S : 'R|P', 2603 ZA : 'a|G|J|M|O|P', 2604 R : 'a|H|K|N|O', 2605 Q : 'noscript|P', 2606 P : 'ins|del|script', 2607 O : 'input|select|textarea|label|button', 2608 N : 'M|L', 2609 M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', 2610 L : 'sub|sup', 2611 K : 'J|I', 2612 J : 'tt|i|b|u|s|strike', 2613 I : 'big|small|font|basefont', 2614 H : 'G|F', 2615 G : 'br|span|bdo', 2616 F : 'object|applet|img|map|iframe', 2617 E : 'A|B|C', 2618 D : 'accesskey|tabindex|onfocus|onblur', 2619 C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', 2620 B : 'lang|xml:lang|dir', 2621 A : 'id|class|style|title' 2622 }, 'script[id|charset|type|language|src|defer|xml:space][]' + 2623 'style[B|id|type|media|title|xml:space][]' + 2624 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' + 2625 'param[id|name|value|valuetype|type][]' + 2626 'p[E|align][#|S]' + 2627 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' + 2628 'br[A|clear][]' + 2629 'span[E][#|S]' + 2630 'bdo[A|C|B][#|S]' + 2631 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' + 2632 'h1[E|align][#|S]' + 2633 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' + 2634 'map[B|C|A|name][X|form|Q|area]' + 2635 'h2[E|align][#|S]' + 2636 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' + 2637 'h3[E|align][#|S]' + 2638 'tt[E][#|S]' + 2639 'i[E][#|S]' + 2640 'b[E][#|S]' + 2641 'u[E][#|S]' + 2642 's[E][#|S]' + 2643 'strike[E][#|S]' + 2644 'big[E][#|S]' + 2645 'small[E][#|S]' + 2646 'font[A|B|size|color|face][#|S]' + 2647 'basefont[id|size|color|face][]' + 2648 'em[E][#|S]' + 2649 'strong[E][#|S]' + 2650 'dfn[E][#|S]' + 2651 'code[E][#|S]' + 2652 'q[E|cite][#|S]' + 2653 'samp[E][#|S]' + 2654 'kbd[E][#|S]' + 2655 'var[E][#|S]' + 2656 'cite[E][#|S]' + 2657 'abbr[E][#|S]' + 2658 'acronym[E][#|S]' + 2659 'sub[E][#|S]' + 2660 'sup[E][#|S]' + 2661 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' + 2662 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' + 2663 'optgroup[E|disabled|label][option]' + 2664 'option[E|selected|disabled|label|value][]' + 2665 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' + 2666 'label[E|for|accesskey|onfocus|onblur][#|S]' + 2667 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + 2668 'h4[E|align][#|S]' + 2669 'ins[E|cite|datetime][#|Y]' + 2670 'h5[E|align][#|S]' + 2671 'del[E|cite|datetime][#|Y]' + 2672 'h6[E|align][#|S]' + 2673 'div[E|align][#|Y]' + 2674 'ul[E|type|compact][li]' + 2675 'li[E|type|value][#|Y]' + 2676 'ol[E|type|compact|start][li]' + 2677 'dl[E|compact][dt|dd]' + 2678 'dt[E][#|S]' + 2679 'dd[E][#|Y]' + 2680 'menu[E|compact][li]' + 2681 'dir[E|compact][li]' + 2682 'pre[E|width|xml:space][#|ZA]' + 2683 'hr[E|align|noshade|size|width][]' + 2684 'blockquote[E|cite][#|Y]' + 2685 'address[E][#|S|p]' + 2686 'center[E][#|Y]' + 2687 'noframes[E][#|Y]' + 2688 'isindex[A|B|prompt][]' + 2689 'fieldset[E][#|legend|Y]' + 2690 'legend[E|accesskey|align][#|S]' + 2691 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' + 2692 'caption[E|align][#|S]' + 2693 'col[ZG][]' + 2694 'colgroup[ZG][col]' + 2695 'thead[ZF][tr]' + 2696 'tr[ZF|bgcolor][th|td]' + 2697 'th[E|ZE][#|Y]' + 2698 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' + 2699 'noscript[E][#|Y]' + 2700 'td[E|ZE][#|Y]' + 2701 'tfoot[ZF][tr]' + 2702 'tbody[ZF][tr]' + 2703 'area[E|D|shape|coords|href|nohref|alt|target][]' + 2704 'base[id|href|target][]' + 2705 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]' 2706 ); 2707 } 2708 2709 return html4; 2710 }; 2711 2712 tinymce.html.Schema = function(settings) { 2713 var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems; 2714 var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, blockElementsMap, nonEmptyElementsMap, customElementsMap = {}; 2715 2716 // Creates an lookup table map object for the specified option or the default value 2717 function createLookupTable(option, default_value, extend) { 2718 var value = settings[option]; 2719 2720 if (!value) { 2721 // Get cached default map or make it if needed 2722 value = mapCache[option]; 2723 2724 if (!value) { 2725 value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' ')); 2726 value = tinymce.extend(value, extend); 2727 2728 mapCache[option] = value; 2729 } 2730 } else { 2731 // Create custom map 2732 value = makeMap(value, ',', makeMap(value.toUpperCase(), ' ')); 2733 } 2734 2735 return value; 2736 }; 2737 2738 settings = settings || {}; 2739 schemaItems = settings.schema == "html5" ? getHTML5() : getHTML4(); 2740 2741 // Allow all elements and attributes if verify_html is set to false 2742 if (settings.verify_html === false) 2743 settings.valid_elements = '*[*]'; 2744 2745 // Build styles list 2746 if (settings.valid_styles) { 2747 validStyles = {}; 2748 2749 // Convert styles into a rule list 2750 each(settings.valid_styles, function(value, key) { 2751 validStyles[key] = tinymce.explode(value); 2752 }); 2753 } 2754 2755 // Setup map objects 2756 whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script style textarea'); 2757 selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); 2758 shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source wbr'); 2759 boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls'); 2760 nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap); 2761 blockElementsMap = createLookupTable('block_elements', 'h1 h2 h3 h4 h5 h6 hr p div address pre form table tbody thead tfoot ' + 2762 'th tr td li ol ul caption blockquote center dl dt dd dir fieldset ' + 2763 'noscript menu isindex samp header footer article section hgroup aside nav figure option datalist select optgroup'); 2764 2765 // Converts a wildcard expression string to a regexp for example *a will become /.*a/. 2766 function patternToRegExp(str) { 2767 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); 2768 }; 2769 2770 // Parses the specified valid_elements string and adds to the current rules 2771 // This function is a bit hard to read since it's heavily optimized for speed 2772 function addValidElements(valid_elements) { 2773 var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, 2774 prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value, 2775 elementRuleRegExp = /^([#+\-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/, 2776 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, 2777 hasPatternsRegExp = /[*?+]/; 2778 2779 if (valid_elements) { 2780 // Split valid elements into an array with rules 2781 valid_elements = split(valid_elements); 2782 2783 if (elements['@']) { 2784 globalAttributes = elements['@'].attributes; 2785 globalAttributesOrder = elements['@'].attributesOrder; 2786 } 2787 2788 // Loop all rules 2789 for (ei = 0, el = valid_elements.length; ei < el; ei++) { 2790 // Parse element rule 2791 matches = elementRuleRegExp.exec(valid_elements[ei]); 2792 if (matches) { 2793 // Setup local names for matches 2794 prefix = matches[1]; 2795 elementName = matches[2]; 2796 outputName = matches[3]; 2797 attrData = matches[4]; 2798 2799 // Create new attributes and attributesOrder 2800 attributes = {}; 2801 attributesOrder = []; 2802 2803 // Create the new element 2804 element = { 2805 attributes : attributes, 2806 attributesOrder : attributesOrder 2807 }; 2808 2809 // Padd empty elements prefix 2810 if (prefix === '#') 2811 element.paddEmpty = true; 2812 2813 // Remove empty elements prefix 2814 if (prefix === '-') 2815 element.removeEmpty = true; 2816 2817 // Copy attributes from global rule into current rule 2818 if (globalAttributes) { 2819 for (key in globalAttributes) 2820 attributes[key] = globalAttributes[key]; 2821 2822 attributesOrder.push.apply(attributesOrder, globalAttributesOrder); 2823 } 2824 2825 // Attributes defined 2826 if (attrData) { 2827 attrData = split(attrData, '|'); 2828 for (ai = 0, al = attrData.length; ai < al; ai++) { 2829 matches = attrRuleRegExp.exec(attrData[ai]); 2830 if (matches) { 2831 attr = {}; 2832 attrType = matches[1]; 2833 attrName = matches[2].replace(/::/g, ':'); 2834 prefix = matches[3]; 2835 value = matches[4]; 2836 2837 // Required 2838 if (attrType === '!') { 2839 element.attributesRequired = element.attributesRequired || []; 2840 element.attributesRequired.push(attrName); 2841 attr.required = true; 2842 } 2843 2844 // Denied from global 2845 if (attrType === '-') { 2846 delete attributes[attrName]; 2847 attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1); 2848 continue; 2849 } 2850 2851 // Default value 2852 if (prefix) { 2853 // Default value 2854 if (prefix === '=') { 2855 element.attributesDefault = element.attributesDefault || []; 2856 element.attributesDefault.push({name: attrName, value: value}); 2857 attr.defaultValue = value; 2858 } 2859 2860 // Forced value 2861 if (prefix === ':') { 2862 element.attributesForced = element.attributesForced || []; 2863 element.attributesForced.push({name: attrName, value: value}); 2864 attr.forcedValue = value; 2865 } 2866 2867 // Required values 2868 if (prefix === '<') 2869 attr.validValues = makeMap(value, '?'); 2870 } 2871 2872 // Check for attribute patterns 2873 if (hasPatternsRegExp.test(attrName)) { 2874 element.attributePatterns = element.attributePatterns || []; 2875 attr.pattern = patternToRegExp(attrName); 2876 element.attributePatterns.push(attr); 2877 } else { 2878 // Add attribute to order list if it doesn't already exist 2879 if (!attributes[attrName]) 2880 attributesOrder.push(attrName); 2881 2882 attributes[attrName] = attr; 2883 } 2884 } 2885 } 2886 } 2887 2888 // Global rule, store away these for later usage 2889 if (!globalAttributes && elementName == '@') { 2890 globalAttributes = attributes; 2891 globalAttributesOrder = attributesOrder; 2892 } 2893 2894 // Handle substitute elements such as b/strong 2895 if (outputName) { 2896 element.outputName = elementName; 2897 elements[outputName] = element; 2898 } 2899 2900 // Add pattern or exact element 2901 if (hasPatternsRegExp.test(elementName)) { 2902 element.pattern = patternToRegExp(elementName); 2903 patternElements.push(element); 2904 } else 2905 elements[elementName] = element; 2906 } 2907 } 2908 } 2909 }; 2910 2911 function setValidElements(valid_elements) { 2912 elements = {}; 2913 patternElements = []; 2914 2915 addValidElements(valid_elements); 2916 2917 each(schemaItems, function(element, name) { 2918 children[name] = element.children; 2919 }); 2920 }; 2921 2922 // Adds custom non HTML elements to the schema 2923 function addCustomElements(custom_elements) { 2924 var customElementRegExp = /^(~)?(.+)$/; 2925 2926 if (custom_elements) { 2927 each(split(custom_elements), function(rule) { 2928 var matches = customElementRegExp.exec(rule), 2929 inline = matches[1] === '~', 2930 cloneName = inline ? 'span' : 'div', 2931 name = matches[2]; 2932 2933 children[name] = children[cloneName]; 2934 customElementsMap[name] = cloneName; 2935 2936 // If it's not marked as inline then add it to valid block elements 2937 if (!inline) 2938 blockElementsMap[name] = {}; 2939 2940 // Add custom elements at span/div positions 2941 each(children, function(element, child) { 2942 if (element[cloneName]) 2943 element[name] = element[cloneName]; 2944 }); 2945 }); 2946 } 2947 }; 2948 2949 // Adds valid children to the schema object 2950 function addValidChildren(valid_children) { 2951 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; 2952 2953 if (valid_children) { 2954 each(split(valid_children), function(rule) { 2955 var matches = childRuleRegExp.exec(rule), parent, prefix; 2956 2957 if (matches) { 2958 prefix = matches[1]; 2959 2960 // Add/remove items from default 2961 if (prefix) 2962 parent = children[matches[2]]; 2963 else 2964 parent = children[matches[2]] = {'#comment' : {}}; 2965 2966 parent = children[matches[2]]; 2967 2968 each(split(matches[3], '|'), function(child) { 2969 if (prefix === '-') 2970 delete parent[child]; 2971 else 2972 parent[child] = {}; 2973 }); 2974 } 2975 }); 2976 } 2977 }; 2978 2979 function getElementRule(name) { 2980 var element = elements[name], i; 2981 2982 // Exact match found 2983 if (element) 2984 return element; 2985 2986 // No exact match then try the patterns 2987 i = patternElements.length; 2988 while (i--) { 2989 element = patternElements[i]; 2990 2991 if (element.pattern.test(name)) 2992 return element; 2993 } 2994 }; 2995 2996 if (!settings.valid_elements) { 2997 // No valid elements defined then clone the elements from the schema spec 2998 each(schemaItems, function(element, name) { 2999 elements[name] = { 3000 attributes : element.attributes, 3001 attributesOrder : element.attributesOrder 3002 }; 3003 3004 children[name] = element.children; 3005 }); 3006 3007 // Switch these on HTML4 3008 if (settings.schema != "html5") { 3009 each(split('strong/b,em/i'), function(item) { 3010 item = split(item, '/'); 3011 elements[item[1]].outputName = item[0]; 3012 }); 3013 } 3014 3015 // Add default alt attribute for images 3016 elements.img.attributesDefault = [{name: 'alt', value: ''}]; 3017 3018 // Remove these if they are empty by default 3019 each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr,strong,em,b,i'), function(name) { 3020 if (elements[name]) { 3021 elements[name].removeEmpty = true; 3022 } 3023 }); 3024 3025 // Padd these by default 3026 each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) { 3027 elements[name].paddEmpty = true; 3028 }); 3029 } else 3030 setValidElements(settings.valid_elements); 3031 3032 addCustomElements(settings.custom_elements); 3033 addValidChildren(settings.valid_children); 3034 addValidElements(settings.extended_valid_elements); 3035 3036 // Todo: Remove this when we fix list handling to be valid 3037 addValidChildren('+ol[ul|ol],+ul[ul|ol]'); 3038 3039 // Delete invalid elements 3040 if (settings.invalid_elements) { 3041 tinymce.each(tinymce.explode(settings.invalid_elements), function(item) { 3042 if (elements[item]) 3043 delete elements[item]; 3044 }); 3045 } 3046 3047 // If the user didn't allow span only allow internal spans 3048 if (!getElementRule('span')) 3049 addValidElements('span[!data-mce-type|*]'); 3050 3051 self.children = children; 3052 3053 self.styles = validStyles; 3054 3055 self.getBoolAttrs = function() { 3056 return boolAttrMap; 3057 }; 3058 3059 self.getBlockElements = function() { 3060 return blockElementsMap; 3061 }; 3062 3063 self.getShortEndedElements = function() { 3064 return shortEndedElementsMap; 3065 }; 3066 3067 self.getSelfClosingElements = function() { 3068 return selfClosingElementsMap; 3069 }; 3070 3071 self.getNonEmptyElements = function() { 3072 return nonEmptyElementsMap; 3073 }; 3074 3075 self.getWhiteSpaceElements = function() { 3076 return whiteSpaceElementsMap; 3077 }; 3078 3079 self.isValidChild = function(name, child) { 3080 var parent = children[name]; 3081 3082 return !!(parent && parent[child]); 3083 }; 3084 3085 self.isValid = function(name, attr) { 3086 var attrPatterns, i, rule = getElementRule(name); 3087 3088 // Check if it's a valid element 3089 if (rule) { 3090 if (attr) { 3091 // Check if attribute name exists 3092 if (rule.attributes[attr]) { 3093 return true; 3094 } 3095 3096 // Check if attribute matches a regexp pattern 3097 attrPatterns = rule.attributePatterns; 3098 if (attrPatterns) { 3099 i = attrPatterns.length; 3100 while (i--) { 3101 if (attrPatterns[i].pattern.test(name)) { 3102 return true; 3103 } 3104 } 3105 } 3106 } else { 3107 return true; 3108 } 3109 } 3110 3111 // No match 3112 return false; 3113 }; 3114 3115 self.getElementRule = getElementRule; 3116 3117 self.getCustomElements = function() { 3118 return customElementsMap; 3119 }; 3120 3121 self.addValidElements = addValidElements; 3122 3123 self.setValidElements = setValidElements; 3124 3125 self.addCustomElements = addCustomElements; 3126 3127 self.addValidChildren = addValidChildren; 3128 }; 3129 })(tinymce); 3130 3131 (function(tinymce) { 3132 tinymce.html.SaxParser = function(settings, schema) { 3133 var self = this, noop = function() {}; 3134 3135 settings = settings || {}; 3136 self.schema = schema = schema || new tinymce.html.Schema(); 3137 3138 if (settings.fix_self_closing !== false) 3139 settings.fix_self_closing = true; 3140 3141 // Add handler functions from settings and setup default handlers 3142 tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) { 3143 if (name) 3144 self[name] = settings[name] || noop; 3145 }); 3146 3147 self.parse = function(html) { 3148 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements, 3149 shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp, 3150 validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing, 3151 tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE; 3152 3153 function processEndTag(name) { 3154 var pos, i; 3155 3156 // Find position of parent of the same type 3157 pos = stack.length; 3158 while (pos--) { 3159 if (stack[pos].name === name) 3160 break; 3161 } 3162 3163 // Found parent 3164 if (pos >= 0) { 3165 // Close all the open elements 3166 for (i = stack.length - 1; i >= pos; i--) { 3167 name = stack[i]; 3168 3169 if (name.valid) 3170 self.end(name.name); 3171 } 3172 3173 // Remove the open elements from the stack 3174 stack.length = pos; 3175 } 3176 }; 3177 3178 function parseAttribute(match, name, value, val2, val3) { 3179 var attrRule, i; 3180 3181 name = name.toLowerCase(); 3182 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute 3183 3184 // Validate name and value 3185 if (validate && !isInternalElement && name.indexOf('data-') !== 0) { 3186 attrRule = validAttributesMap[name]; 3187 3188 // Find rule by pattern matching 3189 if (!attrRule && validAttributePatterns) { 3190 i = validAttributePatterns.length; 3191 while (i--) { 3192 attrRule = validAttributePatterns[i]; 3193 if (attrRule.pattern.test(name)) 3194 break; 3195 } 3196 3197 // No rule matched 3198 if (i === -1) 3199 attrRule = null; 3200 } 3201 3202 // No attribute rule found 3203 if (!attrRule) 3204 return; 3205 3206 // Validate value 3207 if (attrRule.validValues && !(value in attrRule.validValues)) 3208 return; 3209 } 3210 3211 // Add attribute to list and map 3212 attrList.map[name] = value; 3213 attrList.push({ 3214 name: name, 3215 value: value 3216 }); 3217 }; 3218 3219 // Precompile RegExps and map objects 3220 tokenRegExp = new RegExp('<(?:' + 3221 '(?:!--([\\w\\W]*?)-->)|' + // Comment 3222 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA 3223 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE 3224 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI 3225 '(?:\\/([^>]+)>)|' + // End element 3226 '(?:([A-Za-z0-9\\-\\:]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element 3227 ')', 'g'); 3228 3229 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; 3230 specialElements = { 3231 'script' : /<\/script[^>]*>/gi, 3232 'style' : /<\/style[^>]*>/gi, 3233 'noscript' : /<\/noscript[^>]*>/gi 3234 }; 3235 3236 // Setup lookup tables for empty elements and boolean attributes 3237 shortEndedElements = schema.getShortEndedElements(); 3238 selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); 3239 fillAttrsMap = schema.getBoolAttrs(); 3240 validate = settings.validate; 3241 removeInternalElements = settings.remove_internals; 3242 fixSelfClosing = settings.fix_self_closing; 3243 isIE = tinymce.isIE; 3244 invalidPrefixRegExp = /^:/; 3245 3246 while (matches = tokenRegExp.exec(html)) { 3247 // Text 3248 if (index < matches.index) 3249 self.text(decode(html.substr(index, matches.index - index))); 3250 3251 if (value = matches[6]) { // End element 3252 value = value.toLowerCase(); 3253 3254 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 3255 if (isIE && invalidPrefixRegExp.test(value)) 3256 value = value.substr(1); 3257 3258 processEndTag(value); 3259 } else if (value = matches[7]) { // Start element 3260 value = value.toLowerCase(); 3261 3262 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 3263 if (isIE && invalidPrefixRegExp.test(value)) 3264 value = value.substr(1); 3265 3266 isShortEnded = value in shortEndedElements; 3267 3268 // Is self closing tag for example an <li> after an open <li> 3269 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) 3270 processEndTag(value); 3271 3272 // Validate element 3273 if (!validate || (elementRule = schema.getElementRule(value))) { 3274 isValidElement = true; 3275 3276 // Grab attributes map and patters when validation is enabled 3277 if (validate) { 3278 validAttributesMap = elementRule.attributes; 3279 validAttributePatterns = elementRule.attributePatterns; 3280 } 3281 3282 // Parse attributes 3283 if (attribsValue = matches[8]) { 3284 isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element 3285 3286 // If the element has internal attributes then remove it if we are told to do so 3287 if (isInternalElement && removeInternalElements) 3288 isValidElement = false; 3289 3290 attrList = []; 3291 attrList.map = {}; 3292 3293 attribsValue.replace(attrRegExp, parseAttribute); 3294 } else { 3295 attrList = []; 3296 attrList.map = {}; 3297 } 3298 3299 // Process attributes if validation is enabled 3300 if (validate && !isInternalElement) { 3301 attributesRequired = elementRule.attributesRequired; 3302 attributesDefault = elementRule.attributesDefault; 3303 attributesForced = elementRule.attributesForced; 3304 3305 // Handle forced attributes 3306 if (attributesForced) { 3307 i = attributesForced.length; 3308 while (i--) { 3309 attr = attributesForced[i]; 3310 name = attr.name; 3311 attrValue = attr.value; 3312 3313 if (attrValue === '{$uid}') 3314 attrValue = 'mce_' + idCount++; 3315 3316 attrList.map[name] = attrValue; 3317 attrList.push({name: name, value: attrValue}); 3318 } 3319 } 3320 3321 // Handle default attributes 3322 if (attributesDefault) { 3323 i = attributesDefault.length; 3324 while (i--) { 3325 attr = attributesDefault[i]; 3326 name = attr.name; 3327 3328 if (!(name in attrList.map)) { 3329 attrValue = attr.value; 3330 3331 if (attrValue === '{$uid}') 3332 attrValue = 'mce_' + idCount++; 3333 3334 attrList.map[name] = attrValue; 3335 attrList.push({name: name, value: attrValue}); 3336 } 3337 } 3338 } 3339 3340 // Handle required attributes 3341 if (attributesRequired) { 3342 i = attributesRequired.length; 3343 while (i--) { 3344 if (attributesRequired[i] in attrList.map) 3345 break; 3346 } 3347 3348 // None of the required attributes where found 3349 if (i === -1) 3350 isValidElement = false; 3351 } 3352 3353 // Invalidate element if it's marked as bogus 3354 if (attrList.map['data-mce-bogus']) 3355 isValidElement = false; 3356 } 3357 3358 if (isValidElement) 3359 self.start(value, attrList, isShortEnded); 3360 } else 3361 isValidElement = false; 3362 3363 // Treat script, noscript and style a bit different since they may include code that looks like elements 3364 if (endRegExp = specialElements[value]) { 3365 endRegExp.lastIndex = index = matches.index + matches[0].length; 3366 3367 if (matches = endRegExp.exec(html)) { 3368 if (isValidElement) 3369 text = html.substr(index, matches.index - index); 3370 3371 index = matches.index + matches[0].length; 3372 } else { 3373 text = html.substr(index); 3374 index = html.length; 3375 } 3376 3377 if (isValidElement && text.length > 0) 3378 self.text(text, true); 3379 3380 if (isValidElement) 3381 self.end(value); 3382 3383 tokenRegExp.lastIndex = index; 3384 continue; 3385 } 3386 3387 // Push value on to stack 3388 if (!isShortEnded) { 3389 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) 3390 stack.push({name: value, valid: isValidElement}); 3391 else if (isValidElement) 3392 self.end(value); 3393 } 3394 } else if (value = matches[1]) { // Comment 3395 self.comment(value); 3396 } else if (value = matches[2]) { // CDATA 3397 self.cdata(value); 3398 } else if (value = matches[3]) { // DOCTYPE 3399 self.doctype(value); 3400 } else if (value = matches[4]) { // PI 3401 self.pi(value, matches[5]); 3402 } 3403 3404 index = matches.index + matches[0].length; 3405 } 3406 3407 // Text 3408 if (index < html.length) 3409 self.text(decode(html.substr(index))); 3410 3411 // Close any open elements 3412 for (i = stack.length - 1; i >= 0; i--) { 3413 value = stack[i]; 3414 3415 if (value.valid) 3416 self.end(value.name); 3417 } 3418 }; 3419 } 3420 })(tinymce); 3421 3422 (function(tinymce) { 3423 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { 3424 '#text' : 3, 3425 '#comment' : 8, 3426 '#cdata' : 4, 3427 '#pi' : 7, 3428 '#doctype' : 10, 3429 '#document-fragment' : 11 3430 }; 3431 3432 // Walks the tree left/right 3433 function walk(node, root_node, prev) { 3434 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; 3435 3436 // Walk into nodes if it has a start 3437 if (node[startName]) 3438 return node[startName]; 3439 3440 // Return the sibling if it has one 3441 if (node !== root_node) { 3442 sibling = node[siblingName]; 3443 3444 if (sibling) 3445 return sibling; 3446 3447 // Walk up the parents to look for siblings 3448 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { 3449 sibling = parent[siblingName]; 3450 3451 if (sibling) 3452 return sibling; 3453 } 3454 } 3455 }; 3456 3457 function Node(name, type) { 3458 this.name = name; 3459 this.type = type; 3460 3461 if (type === 1) { 3462 this.attributes = []; 3463 this.attributes.map = {}; 3464 } 3465 } 3466 3467 tinymce.extend(Node.prototype, { 3468 replace : function(node) { 3469 var self = this; 3470 3471 if (node.parent) 3472 node.remove(); 3473 3474 self.insert(node, self); 3475 self.remove(); 3476 3477 return self; 3478 }, 3479 3480 attr : function(name, value) { 3481 var self = this, attrs, i, undef; 3482 3483 if (typeof name !== "string") { 3484 for (i in name) 3485 self.attr(i, name[i]); 3486 3487 return self; 3488 } 3489 3490 if (attrs = self.attributes) { 3491 if (value !== undef) { 3492 // Remove attribute 3493 if (value === null) { 3494 if (name in attrs.map) { 3495 delete attrs.map[name]; 3496 3497 i = attrs.length; 3498 while (i--) { 3499 if (attrs[i].name === name) { 3500 attrs = attrs.splice(i, 1); 3501 return self; 3502 } 3503 } 3504 } 3505 3506 return self; 3507 } 3508 3509 // Set attribute 3510 if (name in attrs.map) { 3511 // Set attribute 3512 i = attrs.length; 3513 while (i--) { 3514 if (attrs[i].name === name) { 3515 attrs[i].value = value; 3516 break; 3517 } 3518 } 3519 } else 3520 attrs.push({name: name, value: value}); 3521 3522 attrs.map[name] = value; 3523 3524 return self; 3525 } else { 3526 return attrs.map[name]; 3527 } 3528 } 3529 }, 3530 3531 clone : function() { 3532 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; 3533 3534 // Clone element attributes 3535 if (selfAttrs = self.attributes) { 3536 cloneAttrs = []; 3537 cloneAttrs.map = {}; 3538 3539 for (i = 0, l = selfAttrs.length; i < l; i++) { 3540 selfAttr = selfAttrs[i]; 3541 3542 // Clone everything except id 3543 if (selfAttr.name !== 'id') { 3544 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; 3545 cloneAttrs.map[selfAttr.name] = selfAttr.value; 3546 } 3547 } 3548 3549 clone.attributes = cloneAttrs; 3550 } 3551 3552 clone.value = self.value; 3553 clone.shortEnded = self.shortEnded; 3554 3555 return clone; 3556 }, 3557 3558 wrap : function(wrapper) { 3559 var self = this; 3560 3561 self.parent.insert(wrapper, self); 3562 wrapper.append(self); 3563 3564 return self; 3565 }, 3566 3567 unwrap : function() { 3568 var self = this, node, next; 3569 3570 for (node = self.firstChild; node; ) { 3571 next = node.next; 3572 self.insert(node, self, true); 3573 node = next; 3574 } 3575 3576 self.remove(); 3577 }, 3578 3579 remove : function() { 3580 var self = this, parent = self.parent, next = self.next, prev = self.prev; 3581 3582 if (parent) { 3583 if (parent.firstChild === self) { 3584 parent.firstChild = next; 3585 3586 if (next) 3587 next.prev = null; 3588 } else { 3589 prev.next = next; 3590 } 3591 3592 if (parent.lastChild === self) { 3593 parent.lastChild = prev; 3594 3595 if (prev) 3596 prev.next = null; 3597 } else { 3598 next.prev = prev; 3599 } 3600 3601 self.parent = self.next = self.prev = null; 3602 } 3603 3604 return self; 3605 }, 3606 3607 append : function(node) { 3608 var self = this, last; 3609 3610 if (node.parent) 3611 node.remove(); 3612 3613 last = self.lastChild; 3614 if (last) { 3615 last.next = node; 3616 node.prev = last; 3617 self.lastChild = node; 3618 } else 3619 self.lastChild = self.firstChild = node; 3620 3621 node.parent = self; 3622 3623 return node; 3624 }, 3625 3626 insert : function(node, ref_node, before) { 3627 var parent; 3628 3629 if (node.parent) 3630 node.remove(); 3631 3632 parent = ref_node.parent || this; 3633 3634 if (before) { 3635 if (ref_node === parent.firstChild) 3636 parent.firstChild = node; 3637 else 3638 ref_node.prev.next = node; 3639 3640 node.prev = ref_node.prev; 3641 node.next = ref_node; 3642 ref_node.prev = node; 3643 } else { 3644 if (ref_node === parent.lastChild) 3645 parent.lastChild = node; 3646 else 3647 ref_node.next.prev = node; 3648 3649 node.next = ref_node.next; 3650 node.prev = ref_node; 3651 ref_node.next = node; 3652 } 3653 3654 node.parent = parent; 3655 3656 return node; 3657 }, 3658 3659 getAll : function(name) { 3660 var self = this, node, collection = []; 3661 3662 for (node = self.firstChild; node; node = walk(node, self)) { 3663 if (node.name === name) 3664 collection.push(node); 3665 } 3666 3667 return collection; 3668 }, 3669 3670 empty : function() { 3671 var self = this, nodes, i, node; 3672 3673 // Remove all children 3674 if (self.firstChild) { 3675 nodes = []; 3676 3677 // Collect the children 3678 for (node = self.firstChild; node; node = walk(node, self)) 3679 nodes.push(node); 3680 3681 // Remove the children 3682 i = nodes.length; 3683 while (i--) { 3684 node = nodes[i]; 3685 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; 3686 } 3687 } 3688 3689 self.firstChild = self.lastChild = null; 3690 3691 return self; 3692 }, 3693 3694 isEmpty : function(elements) { 3695 var self = this, node = self.firstChild, i, name; 3696 3697 if (node) { 3698 do { 3699 if (node.type === 1) { 3700 // Ignore bogus elements 3701 if (node.attributes.map['data-mce-bogus']) 3702 continue; 3703 3704 // Keep empty elements like <img /> 3705 if (elements[node.name]) 3706 return false; 3707 3708 // Keep elements with data attributes or name attribute like <a name="1"></a> 3709 i = node.attributes.length; 3710 while (i--) { 3711 name = node.attributes[i].name; 3712 if (name === "name" || name.indexOf('data-') === 0) 3713 return false; 3714 } 3715 } 3716 3717 // Keep comments 3718 if (node.type === 8) 3719 return false; 3720 3721 // Keep non whitespace text nodes 3722 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) 3723 return false; 3724 } while (node = walk(node, self)); 3725 } 3726 3727 return true; 3728 }, 3729 3730 walk : function(prev) { 3731 return walk(this, null, prev); 3732 } 3733 }); 3734 3735 tinymce.extend(Node, { 3736 create : function(name, attrs) { 3737 var node, attrName; 3738 3739 // Create node 3740 node = new Node(name, typeLookup[name] || 1); 3741 3742 // Add attributes if needed 3743 if (attrs) { 3744 for (attrName in attrs) 3745 node.attr(attrName, attrs[attrName]); 3746 } 3747 3748 return node; 3749 } 3750 }); 3751 3752 tinymce.html.Node = Node; 3753 })(tinymce); 3754 3755 (function(tinymce) { 3756 var Node = tinymce.html.Node; 3757 3758 tinymce.html.DomParser = function(settings, schema) { 3759 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; 3760 3761 settings = settings || {}; 3762 settings.validate = "validate" in settings ? settings.validate : true; 3763 settings.root_name = settings.root_name || 'body'; 3764 self.schema = schema = schema || new tinymce.html.Schema(); 3765 3766 function fixInvalidChildren(nodes) { 3767 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i, 3768 childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode; 3769 3770 nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table'); 3771 nonEmptyElements = schema.getNonEmptyElements(); 3772 3773 for (ni = 0; ni < nodes.length; ni++) { 3774 node = nodes[ni]; 3775 3776 // Already removed 3777 if (!node.parent) 3778 continue; 3779 3780 // Get list of all parent nodes until we find a valid parent to stick the child into 3781 parents = [node]; 3782 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent) 3783 parents.push(parent); 3784 3785 // Found a suitable parent 3786 if (parent && parents.length > 1) { 3787 // Reverse the array since it makes looping easier 3788 parents.reverse(); 3789 3790 // Clone the related parent and insert that after the moved node 3791 newParent = currentNode = self.filterNode(parents[0].clone()); 3792 3793 // Start cloning and moving children on the left side of the target node 3794 for (i = 0; i < parents.length - 1; i++) { 3795 if (schema.isValidChild(currentNode.name, parents[i].name)) { 3796 tempNode = self.filterNode(parents[i].clone()); 3797 currentNode.append(tempNode); 3798 } else 3799 tempNode = currentNode; 3800 3801 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) { 3802 nextNode = childNode.next; 3803 tempNode.append(childNode); 3804 childNode = nextNode; 3805 } 3806 3807 currentNode = tempNode; 3808 } 3809 3810 if (!newParent.isEmpty(nonEmptyElements)) { 3811 parent.insert(newParent, parents[0], true); 3812 parent.insert(node, newParent); 3813 } else { 3814 parent.insert(node, parents[0], true); 3815 } 3816 3817 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p> 3818 parent = parents[0]; 3819 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { 3820 parent.empty().remove(); 3821 } 3822 } else if (node.parent) { 3823 // If it's an LI try to find a UL/OL for it or wrap it 3824 if (node.name === 'li') { 3825 sibling = node.prev; 3826 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 3827 sibling.append(node); 3828 continue; 3829 } 3830 3831 sibling = node.next; 3832 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 3833 sibling.insert(node, sibling.firstChild, true); 3834 continue; 3835 } 3836 3837 node.wrap(self.filterNode(new Node('ul', 1))); 3838 continue; 3839 } 3840 3841 // Try wrapping the element in a DIV 3842 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { 3843 node.wrap(self.filterNode(new Node('div', 1))); 3844 } else { 3845 // We failed wrapping it, then remove or unwrap it 3846 if (node.name === 'style' || node.name === 'script') 3847 node.empty().remove(); 3848 else 3849 node.unwrap(); 3850 } 3851 } 3852 } 3853 }; 3854 3855 self.filterNode = function(node) { 3856 var i, name, list; 3857 3858 // Run element filters 3859 if (name in nodeFilters) { 3860 list = matchedNodes[name]; 3861 3862 if (list) 3863 list.push(node); 3864 else 3865 matchedNodes[name] = [node]; 3866 } 3867 3868 // Run attribute filters 3869 i = attributeFilters.length; 3870 while (i--) { 3871 name = attributeFilters[i].name; 3872 3873 if (name in node.attributes.map) { 3874 list = matchedAttributes[name]; 3875 3876 if (list) 3877 list.push(node); 3878 else 3879 matchedAttributes[name] = [node]; 3880 } 3881 } 3882 3883 return node; 3884 }; 3885 3886 self.addNodeFilter = function(name, callback) { 3887 tinymce.each(tinymce.explode(name), function(name) { 3888 var list = nodeFilters[name]; 3889 3890 if (!list) 3891 nodeFilters[name] = list = []; 3892 3893 list.push(callback); 3894 }); 3895 }; 3896 3897 self.addAttributeFilter = function(name, callback) { 3898 tinymce.each(tinymce.explode(name), function(name) { 3899 var i; 3900 3901 for (i = 0; i < attributeFilters.length; i++) { 3902 if (attributeFilters[i].name === name) { 3903 attributeFilters[i].callbacks.push(callback); 3904 return; 3905 } 3906 } 3907 3908 attributeFilters.push({name: name, callbacks: [callback]}); 3909 }); 3910 }; 3911 3912 self.parse = function(html, args) { 3913 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate, 3914 blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement, 3915 endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; 3916 3917 args = args || {}; 3918 matchedNodes = {}; 3919 matchedAttributes = {}; 3920 blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); 3921 nonEmptyElements = schema.getNonEmptyElements(); 3922 children = schema.children; 3923 validate = settings.validate; 3924 rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; 3925 3926 whiteSpaceElements = schema.getWhiteSpaceElements(); 3927 startWhiteSpaceRegExp = /^[ \t\r\n]+/; 3928 endWhiteSpaceRegExp = /[ \t\r\n]+$/; 3929 allWhiteSpaceRegExp = /[ \t\r\n]+/g; 3930 isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; 3931 3932 function addRootBlocks() { 3933 var node = rootNode.firstChild, next, rootBlockNode; 3934 3935 while (node) { 3936 next = node.next; 3937 3938 if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) { 3939 if (!rootBlockNode) { 3940 // Create a new root block element 3941 rootBlockNode = createNode(rootBlockName, 1); 3942 rootNode.insert(rootBlockNode, node); 3943 rootBlockNode.append(node); 3944 } else 3945 rootBlockNode.append(node); 3946 } else { 3947 rootBlockNode = null; 3948 } 3949 3950 node = next; 3951 }; 3952 }; 3953 3954 function createNode(name, type) { 3955 var node = new Node(name, type), list; 3956 3957 if (name in nodeFilters) { 3958 list = matchedNodes[name]; 3959 3960 if (list) 3961 list.push(node); 3962 else 3963 matchedNodes[name] = [node]; 3964 } 3965 3966 return node; 3967 }; 3968 3969 function removeWhitespaceBefore(node) { 3970 var textNode, textVal, sibling; 3971 3972 for (textNode = node.prev; textNode && textNode.type === 3; ) { 3973 textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); 3974 3975 if (textVal.length > 0) { 3976 textNode.value = textVal; 3977 textNode = textNode.prev; 3978 } else { 3979 sibling = textNode.prev; 3980 textNode.remove(); 3981 textNode = sibling; 3982 } 3983 } 3984 }; 3985 3986 function cloneAndExcludeBlocks(input) { 3987 var name, output = {}; 3988 3989 for (name in input) { 3990 if (name !== 'li' && name != 'p') { 3991 output[name] = input[name]; 3992 } 3993 } 3994 3995 return output; 3996 }; 3997 3998 parser = new tinymce.html.SaxParser({ 3999 validate : validate, 4000 4001 // Exclude P and LI from DOM parsing since it's treated better by the DOM parser 4002 self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), 4003 4004 cdata: function(text) { 4005 node.append(createNode('#cdata', 4)).value = text; 4006 }, 4007 4008 text: function(text, raw) { 4009 var textNode; 4010 4011 // Trim all redundant whitespace on non white space elements 4012 if (!isInWhiteSpacePreservedElement) { 4013 text = text.replace(allWhiteSpaceRegExp, ' '); 4014 4015 if (node.lastChild && blockElements[node.lastChild.name]) 4016 text = text.replace(startWhiteSpaceRegExp, ''); 4017 } 4018 4019 // Do we need to create the node 4020 if (text.length !== 0) { 4021 textNode = createNode('#text', 3); 4022 textNode.raw = !!raw; 4023 node.append(textNode).value = text; 4024 } 4025 }, 4026 4027 comment: function(text) { 4028 node.append(createNode('#comment', 8)).value = text; 4029 }, 4030 4031 pi: function(name, text) { 4032 node.append(createNode(name, 7)).value = text; 4033 removeWhitespaceBefore(node); 4034 }, 4035 4036 doctype: function(text) { 4037 var newNode; 4038 4039 newNode = node.append(createNode('#doctype', 10)); 4040 newNode.value = text; 4041 removeWhitespaceBefore(node); 4042 }, 4043 4044 start: function(name, attrs, empty) { 4045 var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent; 4046 4047 elementRule = validate ? schema.getElementRule(name) : {}; 4048 if (elementRule) { 4049 newNode = createNode(elementRule.outputName || name, 1); 4050 newNode.attributes = attrs; 4051 newNode.shortEnded = empty; 4052 4053 node.append(newNode); 4054 4055 // Check if node is valid child of the parent node is the child is 4056 // unknown we don't collect it since it's probably a custom element 4057 parent = children[node.name]; 4058 if (parent && children[newNode.name] && !parent[newNode.name]) 4059 invalidChildren.push(newNode); 4060 4061 attrFiltersLen = attributeFilters.length; 4062 while (attrFiltersLen--) { 4063 attrName = attributeFilters[attrFiltersLen].name; 4064 4065 if (attrName in attrs.map) { 4066 list = matchedAttributes[attrName]; 4067 4068 if (list) 4069 list.push(newNode); 4070 else 4071 matchedAttributes[attrName] = [newNode]; 4072 } 4073 } 4074 4075 // Trim whitespace before block 4076 if (blockElements[name]) 4077 removeWhitespaceBefore(newNode); 4078 4079 // Change current node if the element wasn't empty i.e not <br /> or <img /> 4080 if (!empty) 4081 node = newNode; 4082 4083 // Check if we are inside a whitespace preserved element 4084 if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 4085 isInWhiteSpacePreservedElement = true; 4086 } 4087 } 4088 }, 4089 4090 end: function(name) { 4091 var textNode, elementRule, text, sibling, tempNode; 4092 4093 elementRule = validate ? schema.getElementRule(name) : {}; 4094 if (elementRule) { 4095 if (blockElements[name]) { 4096 if (!isInWhiteSpacePreservedElement) { 4097 // Trim whitespace of the first node in a block 4098 textNode = node.firstChild; 4099 if (textNode && textNode.type === 3) { 4100 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 4101 4102 // Any characters left after trim or should we remove it 4103 if (text.length > 0) { 4104 textNode.value = text; 4105 textNode = textNode.next; 4106 } else { 4107 sibling = textNode.next; 4108 textNode.remove(); 4109 textNode = sibling; 4110 } 4111 4112 // Remove any pure whitespace siblings 4113 while (textNode && textNode.type === 3) { 4114 text = textNode.value; 4115 sibling = textNode.next; 4116 4117 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 4118 textNode.remove(); 4119 textNode = sibling; 4120 } 4121 4122 textNode = sibling; 4123 } 4124 } 4125 4126 // Trim whitespace of the last node in a block 4127 textNode = node.lastChild; 4128 if (textNode && textNode.type === 3) { 4129 text = textNode.value.replace(endWhiteSpaceRegExp, ''); 4130 4131 // Any characters left after trim or should we remove it 4132 if (text.length > 0) { 4133 textNode.value = text; 4134 textNode = textNode.prev; 4135 } else { 4136 sibling = textNode.prev; 4137 textNode.remove(); 4138 textNode = sibling; 4139 } 4140 4141 // Remove any pure whitespace siblings 4142 while (textNode && textNode.type === 3) { 4143 text = textNode.value; 4144 sibling = textNode.prev; 4145 4146 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 4147 textNode.remove(); 4148 textNode = sibling; 4149 } 4150 4151 textNode = sibling; 4152 } 4153 } 4154 } 4155 4156 // Trim start white space 4157 textNode = node.prev; 4158 if (textNode && textNode.type === 3) { 4159 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 4160 4161 if (text.length > 0) 4162 textNode.value = text; 4163 else 4164 textNode.remove(); 4165 } 4166 } 4167 4168 // Check if we exited a whitespace preserved element 4169 if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 4170 isInWhiteSpacePreservedElement = false; 4171 } 4172 4173 // Handle empty nodes 4174 if (elementRule.removeEmpty || elementRule.paddEmpty) { 4175 if (node.isEmpty(nonEmptyElements)) { 4176 if (elementRule.paddEmpty) 4177 node.empty().append(new Node('#text', '3')).value = '\u00a0'; 4178 else { 4179 // Leave nodes that have a name like <a name="name"> 4180 if (!node.attributes.map.name && !node.attributes.map.id) { 4181 tempNode = node.parent; 4182 node.empty().remove(); 4183 node = tempNode; 4184 return; 4185 } 4186 } 4187 } 4188 } 4189 4190 node = node.parent; 4191 } 4192 } 4193 }, schema); 4194 4195 rootNode = node = new Node(args.context || settings.root_name, 11); 4196 4197 parser.parse(html); 4198 4199 // Fix invalid children or report invalid children in a contextual parsing 4200 if (validate && invalidChildren.length) { 4201 if (!args.context) 4202 fixInvalidChildren(invalidChildren); 4203 else 4204 args.invalid = true; 4205 } 4206 4207 // Wrap nodes in the root into block elements if the root is body 4208 if (rootBlockName && rootNode.name == 'body') 4209 addRootBlocks(); 4210 4211 // Run filters only when the contents is valid 4212 if (!args.invalid) { 4213 // Run node filters 4214 for (name in matchedNodes) { 4215 list = nodeFilters[name]; 4216 nodes = matchedNodes[name]; 4217 4218 // Remove already removed children 4219 fi = nodes.length; 4220 while (fi--) { 4221 if (!nodes[fi].parent) 4222 nodes.splice(fi, 1); 4223 } 4224 4225 for (i = 0, l = list.length; i < l; i++) 4226 list[i](nodes, name, args); 4227 } 4228 4229 // Run attribute filters 4230 for (i = 0, l = attributeFilters.length; i < l; i++) { 4231 list = attributeFilters[i]; 4232 4233 if (list.name in matchedAttributes) { 4234 nodes = matchedAttributes[list.name]; 4235 4236 // Remove already removed children 4237 fi = nodes.length; 4238 while (fi--) { 4239 if (!nodes[fi].parent) 4240 nodes.splice(fi, 1); 4241 } 4242 4243 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) 4244 list.callbacks[fi](nodes, list.name, args); 4245 } 4246 } 4247 } 4248 4249 return rootNode; 4250 }; 4251 4252 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to 4253 // make it possible to place the caret inside empty blocks. This logic tries to remove 4254 // these elements and keep br elements that where intended to be there intact 4255 if (settings.remove_trailing_brs) { 4256 self.addNodeFilter('br', function(nodes, name) { 4257 var i, l = nodes.length, node, blockElements = tinymce.extend({}, schema.getBlockElements()), 4258 nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; 4259 4260 // Remove brs from body element as well 4261 blockElements.body = 1; 4262 4263 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p> 4264 for (i = 0; i < l; i++) { 4265 node = nodes[i]; 4266 parent = node.parent; 4267 4268 if (blockElements[node.parent.name] && node === parent.lastChild) { 4269 // Loop all nodes to the left of the current node and check for other BR elements 4270 // excluding bookmarks since they are invisible 4271 prev = node.prev; 4272 while (prev) { 4273 prevName = prev.name; 4274 4275 // Ignore bookmarks 4276 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { 4277 // Found a non BR element 4278 if (prevName !== "br") 4279 break; 4280 4281 // Found another br it's a <br><br> structure then don't remove anything 4282 if (prevName === 'br') { 4283 node = null; 4284 break; 4285 } 4286 } 4287 4288 prev = prev.prev; 4289 } 4290 4291 if (node) { 4292 node.remove(); 4293 4294 // Is the parent to be considered empty after we removed the BR 4295 if (parent.isEmpty(nonEmptyElements)) { 4296 elementRule = schema.getElementRule(parent.name); 4297 4298 // Remove or padd the element depending on schema rule 4299 if (elementRule) { 4300 if (elementRule.removeEmpty) 4301 parent.remove(); 4302 else if (elementRule.paddEmpty) 4303 parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; 4304 } 4305 } 4306 } 4307 } else { 4308 // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p> so they become <p><b><i> </i></b></p> 4309 lastParent = node; 4310 while (parent.firstChild === lastParent && parent.lastChild === lastParent) { 4311 lastParent = parent; 4312 4313 if (blockElements[parent.name]) { 4314 break; 4315 } 4316 4317 parent = parent.parent; 4318 } 4319 4320 if (lastParent === parent) { 4321 textNode = new tinymce.html.Node('#text', 3); 4322 textNode.value = '\u00a0'; 4323 node.replace(textNode); 4324 } 4325 } 4326 } 4327 }); 4328 } 4329 4330 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. 4331 if (!settings.allow_html_in_named_anchor) { 4332 self.addAttributeFilter('id,name', function(nodes, name) { 4333 var i = nodes.length, sibling, prevSibling, parent, node; 4334 4335 while (i--) { 4336 node = nodes[i]; 4337 if (node.name === 'a' && node.firstChild && !node.attr('href')) { 4338 parent = node.parent; 4339 4340 // Move children after current node 4341 sibling = node.lastChild; 4342 do { 4343 prevSibling = sibling.prev; 4344 parent.insert(sibling, node); 4345 sibling = prevSibling; 4346 } while (sibling); 4347 } 4348 } 4349 }); 4350 } 4351 } 4352 })(tinymce); 4353 4354 tinymce.html.Writer = function(settings) { 4355 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; 4356 4357 settings = settings || {}; 4358 indent = settings.indent; 4359 indentBefore = tinymce.makeMap(settings.indent_before || ''); 4360 indentAfter = tinymce.makeMap(settings.indent_after || ''); 4361 encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); 4362 htmlOutput = settings.element_format == "html"; 4363 4364 return { 4365 start: function(name, attrs, empty) { 4366 var i, l, attr, value; 4367 4368 if (indent && indentBefore[name] && html.length > 0) { 4369 value = html[html.length - 1]; 4370 4371 if (value.length > 0 && value !== '\n') 4372 html.push('\n'); 4373 } 4374 4375 html.push('<', name); 4376 4377 if (attrs) { 4378 for (i = 0, l = attrs.length; i < l; i++) { 4379 attr = attrs[i]; 4380 html.push(' ', attr.name, '="', encode(attr.value, true), '"'); 4381 } 4382 } 4383 4384 if (!empty || htmlOutput) 4385 html[html.length] = '>'; 4386 else 4387 html[html.length] = ' />'; 4388 4389 if (empty && indent && indentAfter[name] && html.length > 0) { 4390 value = html[html.length - 1]; 4391 4392 if (value.length > 0 && value !== '\n') 4393 html.push('\n'); 4394 } 4395 }, 4396 4397 end: function(name) { 4398 var value; 4399 4400 /*if (indent && indentBefore[name] && html.length > 0) { 4401 value = html[html.length - 1]; 4402 4403 if (value.length > 0 && value !== '\n') 4404 html.push('\n'); 4405 }*/ 4406 4407 html.push('</', name, '>'); 4408 4409 if (indent && indentAfter[name] && html.length > 0) { 4410 value = html[html.length - 1]; 4411 4412 if (value.length > 0 && value !== '\n') 4413 html.push('\n'); 4414 } 4415 }, 4416 4417 text: function(text, raw) { 4418 if (text.length > 0) 4419 html[html.length] = raw ? text : encode(text); 4420 }, 4421 4422 cdata: function(text) { 4423 html.push('<![CDATA[', text, ']]>'); 4424 }, 4425 4426 comment: function(text) { 4427 html.push('<!--', text, '-->'); 4428 }, 4429 4430 pi: function(name, text) { 4431 if (text) 4432 html.push('<?', name, ' ', text, '?>'); 4433 else 4434 html.push('<?', name, '?>'); 4435 4436 if (indent) 4437 html.push('\n'); 4438 }, 4439 4440 doctype: function(text) { 4441 html.push('<!DOCTYPE', text, '>', indent ? '\n' : ''); 4442 }, 4443 4444 reset: function() { 4445 html.length = 0; 4446 }, 4447 4448 getContent: function() { 4449 return html.join('').replace(/\n$/, ''); 4450 } 4451 }; 4452 }; 4453 4454 (function(tinymce) { 4455 tinymce.html.Serializer = function(settings, schema) { 4456 var self = this, writer = new tinymce.html.Writer(settings); 4457 4458 settings = settings || {}; 4459 settings.validate = "validate" in settings ? settings.validate : true; 4460 4461 self.schema = schema = schema || new tinymce.html.Schema(); 4462 self.writer = writer; 4463 4464 self.serialize = function(node) { 4465 var handlers, validate; 4466 4467 validate = settings.validate; 4468 4469 handlers = { 4470 // #text 4471 3: function(node, raw) { 4472 writer.text(node.value, node.raw); 4473 }, 4474 4475 // #comment 4476 8: function(node) { 4477 writer.comment(node.value); 4478 }, 4479 4480 // Processing instruction 4481 7: function(node) { 4482 writer.pi(node.name, node.value); 4483 }, 4484 4485 // Doctype 4486 10: function(node) { 4487 writer.doctype(node.value); 4488 }, 4489 4490 // CDATA 4491 4: function(node) { 4492 writer.cdata(node.value); 4493 }, 4494 4495 // Document fragment 4496 11: function(node) { 4497 if ((node = node.firstChild)) { 4498 do { 4499 walk(node); 4500 } while (node = node.next); 4501 } 4502 } 4503 }; 4504 4505 writer.reset(); 4506 4507 function walk(node) { 4508 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; 4509 4510 if (!handler) { 4511 name = node.name; 4512 isEmpty = node.shortEnded; 4513 attrs = node.attributes; 4514 4515 // Sort attributes 4516 if (validate && attrs && attrs.length > 1) { 4517 sortedAttrs = []; 4518 sortedAttrs.map = {}; 4519 4520 elementRule = schema.getElementRule(node.name); 4521 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { 4522 attrName = elementRule.attributesOrder[i]; 4523 4524 if (attrName in attrs.map) { 4525 attrValue = attrs.map[attrName]; 4526 sortedAttrs.map[attrName] = attrValue; 4527 sortedAttrs.push({name: attrName, value: attrValue}); 4528 } 4529 } 4530 4531 for (i = 0, l = attrs.length; i < l; i++) { 4532 attrName = attrs[i].name; 4533 4534 if (!(attrName in sortedAttrs.map)) { 4535 attrValue = attrs.map[attrName]; 4536 sortedAttrs.map[attrName] = attrValue; 4537 sortedAttrs.push({name: attrName, value: attrValue}); 4538 } 4539 } 4540 4541 attrs = sortedAttrs; 4542 } 4543 4544 writer.start(node.name, attrs, isEmpty); 4545 4546 if (!isEmpty) { 4547 if ((node = node.firstChild)) { 4548 do { 4549 walk(node); 4550 } while (node = node.next); 4551 } 4552 4553 writer.end(name); 4554 } 4555 } else 4556 handler(node); 4557 } 4558 4559 // Serialize element and treat all non elements as fragments 4560 if (node.type == 1 && !settings.inner) 4561 walk(node); 4562 else 4563 handlers[11](node); 4564 4565 return writer.getContent(); 4566 }; 4567 } 4568 })(tinymce); 4569 4570 // JSLint defined globals 4571 /*global tinymce:false, window:false */ 4572 4573 tinymce.dom = {}; 4574 4575 (function(namespace, expando) { 4576 var w3cEventModel = !!document.addEventListener; 4577 4578 function addEvent(target, name, callback, capture) { 4579 if (target.addEventListener) { 4580 target.addEventListener(name, callback, capture || false); 4581 } else if (target.attachEvent) { 4582 target.attachEvent('on' + name, callback); 4583 } 4584 } 4585 4586 function removeEvent(target, name, callback, capture) { 4587 if (target.removeEventListener) { 4588 target.removeEventListener(name, callback, capture || false); 4589 } else if (target.detachEvent) { 4590 target.detachEvent('on' + name, callback); 4591 } 4592 } 4593 4594 function fix(original_event, data) { 4595 var name, event = data || {}; 4596 4597 // Dummy function that gets replaced on the delegation state functions 4598 function returnFalse() { 4599 return false; 4600 } 4601 4602 // Dummy function that gets replaced on the delegation state functions 4603 function returnTrue() { 4604 return true; 4605 } 4606 4607 // Copy all properties from the original event 4608 for (name in original_event) { 4609 // layerX/layerY is deprecated in Chrome and produces a warning 4610 if (name !== "layerX" && name !== "layerY") { 4611 event[name] = original_event[name]; 4612 } 4613 } 4614 4615 // Normalize target IE uses srcElement 4616 if (!event.target) { 4617 event.target = event.srcElement || document; 4618 } 4619 4620 // Add preventDefault method 4621 event.preventDefault = function() { 4622 event.isDefaultPrevented = returnTrue; 4623 4624 // Execute preventDefault on the original event object 4625 if (original_event) { 4626 if (original_event.preventDefault) { 4627 original_event.preventDefault(); 4628 } else { 4629 original_event.returnValue = false; // IE 4630 } 4631 } 4632 }; 4633 4634 // Add stopPropagation 4635 event.stopPropagation = function() { 4636 event.isPropagationStopped = returnTrue; 4637 4638 // Execute stopPropagation on the original event object 4639 if (original_event) { 4640 if (original_event.stopPropagation) { 4641 original_event.stopPropagation(); 4642 } else { 4643 original_event.cancelBubble = true; // IE 4644 } 4645 } 4646 }; 4647 4648 // Add stopImmediatePropagation 4649 event.stopImmediatePropagation = function() { 4650 event.isImmediatePropagationStopped = returnTrue; 4651 event.stopPropagation(); 4652 }; 4653 4654 // Add event delegation states 4655 if (!event.isDefaultPrevented) { 4656 event.isDefaultPrevented = returnFalse; 4657 event.isPropagationStopped = returnFalse; 4658 event.isImmediatePropagationStopped = returnFalse; 4659 } 4660 4661 return event; 4662 } 4663 4664 function bindOnReady(win, callback, event_utils) { 4665 var doc = win.document, event = {type: 'ready'}; 4666 4667 // Gets called when the DOM is ready 4668 function readyHandler() { 4669 if (!event_utils.domLoaded) { 4670 event_utils.domLoaded = true; 4671 callback(event); 4672 } 4673 } 4674 4675 // Use W3C method 4676 if (w3cEventModel) { 4677 addEvent(win, 'DOMContentLoaded', readyHandler); 4678 } else { 4679 // Use IE method 4680 addEvent(doc, "readystatechange", function() { 4681 if (doc.readyState === "complete") { 4682 removeEvent(doc, "readystatechange", arguments.callee); 4683 readyHandler(); 4684 } 4685 }); 4686 4687 // Wait until we can scroll, when we can the DOM is initialized 4688 if (doc.documentElement.doScroll && win === win.top) { 4689 (function() { 4690 try { 4691 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. 4692 // http://javascript.nwbox.com/IEContentLoaded/ 4693 doc.documentElement.doScroll("left"); 4694 } catch (ex) { 4695 setTimeout(arguments.callee, 0); 4696 return; 4697 } 4698 4699 readyHandler(); 4700 })(); 4701 } 4702 } 4703 4704 // Fallback if any of the above methods should fail for some odd reason 4705 addEvent(win, 'load', readyHandler); 4706 } 4707 4708 function EventUtils(proxy) { 4709 var self = this, events = {}, count, isFocusBlurBound, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave; 4710 4711 hasMouseEnterLeave = "onmouseenter" in document.documentElement; 4712 hasFocusIn = "onfocusin" in document.documentElement; 4713 mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'}; 4714 count = 1; 4715 4716 // State if the DOMContentLoaded was executed or not 4717 self.domLoaded = false; 4718 self.events = events; 4719 4720 function executeHandlers(evt, id) { 4721 var callbackList, i, l, callback; 4722 4723 callbackList = events[id][evt.type]; 4724 if (callbackList) { 4725 for (i = 0, l = callbackList.length; i < l; i++) { 4726 callback = callbackList[i]; 4727 4728 // Check if callback exists might be removed if a unbind is called inside the callback 4729 if (callback && callback.func.call(callback.scope, evt) === false) { 4730 evt.preventDefault(); 4731 } 4732 4733 // Should we stop propagation to immediate listeners 4734 if (evt.isImmediatePropagationStopped()) { 4735 return; 4736 } 4737 } 4738 } 4739 } 4740 4741 self.bind = function(target, names, callback, scope) { 4742 var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window; 4743 4744 // Native event handler function patches the event and executes the callbacks for the expando 4745 function defaultNativeHandler(evt) { 4746 executeHandlers(fix(evt || win.event), id); 4747 } 4748 4749 // Don't bind to text nodes or comments 4750 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4751 return; 4752 } 4753 4754 // Create or get events id for the target 4755 if (!target[expando]) { 4756 id = count++; 4757 target[expando] = id; 4758 events[id] = {}; 4759 } else { 4760 id = target[expando]; 4761 4762 if (!events[id]) { 4763 events[id] = {}; 4764 } 4765 } 4766 4767 // Setup the specified scope or use the target as a default 4768 scope = scope || target; 4769 4770 // Split names and bind each event, enables you to bind multiple events with one call 4771 names = names.split(' '); 4772 i = names.length; 4773 while (i--) { 4774 name = names[i]; 4775 nativeHandler = defaultNativeHandler; 4776 fakeName = capture = false; 4777 4778 // Use ready instead of DOMContentLoaded 4779 if (name === "DOMContentLoaded") { 4780 name = "ready"; 4781 } 4782 4783 // DOM is already ready 4784 if ((self.domLoaded || target.readyState == 'complete') && name === "ready") { 4785 self.domLoaded = true; 4786 callback.call(scope, fix({type: name})); 4787 continue; 4788 } 4789 4790 // Handle mouseenter/mouseleaver 4791 if (!hasMouseEnterLeave) { 4792 fakeName = mouseEnterLeave[name]; 4793 4794 if (fakeName) { 4795 nativeHandler = function(evt) { 4796 var current, related; 4797 4798 current = evt.currentTarget; 4799 related = evt.relatedTarget; 4800 4801 // Check if related is inside the current target if it's not then the event should be ignored since it's a mouseover/mouseout inside the element 4802 if (related && current.contains) { 4803 // Use contains for performance 4804 related = current.contains(related); 4805 } else { 4806 while (related && related !== current) { 4807 related = related.parentNode; 4808 } 4809 } 4810 4811 // Fire fake event 4812 if (!related) { 4813 evt = fix(evt || win.event); 4814 evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter'; 4815 evt.target = current; 4816 executeHandlers(evt, id); 4817 } 4818 }; 4819 } 4820 } 4821 4822 // Fake bubbeling of focusin/focusout 4823 if (!hasFocusIn && (name === "focusin" || name === "focusout")) { 4824 capture = true; 4825 fakeName = name === "focusin" ? "focus" : "blur"; 4826 nativeHandler = function(evt) { 4827 evt = fix(evt || win.event); 4828 evt.type = evt.type === 'focus' ? 'focusin' : 'focusout'; 4829 executeHandlers(evt, id); 4830 }; 4831 } 4832 4833 // Setup callback list and bind native event 4834 callbackList = events[id][name]; 4835 if (!callbackList) { 4836 events[id][name] = callbackList = [{func: callback, scope: scope}]; 4837 callbackList.fakeName = fakeName; 4838 callbackList.capture = capture; 4839 4840 // Add the nativeHandler to the callback list so that we can later unbind it 4841 callbackList.nativeHandler = nativeHandler; 4842 if (!w3cEventModel) { 4843 callbackList.proxyHandler = proxy(id); 4844 } 4845 4846 // Check if the target has native events support 4847 if (name === "ready") { 4848 bindOnReady(target, nativeHandler, self); 4849 } else { 4850 addEvent(target, fakeName || name, w3cEventModel ? nativeHandler : callbackList.proxyHandler, capture); 4851 } 4852 } else { 4853 // If it already has an native handler then just push the callback 4854 callbackList.push({func: callback, scope: scope}); 4855 } 4856 } 4857 4858 target = callbackList = 0; // Clean memory for IE 4859 4860 return callback; 4861 }; 4862 4863 self.unbind = function(target, names, callback) { 4864 var id, callbackList, i, ci, name, eventMap; 4865 4866 // Don't bind to text nodes or comments 4867 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4868 return self; 4869 } 4870 4871 // Unbind event or events if the target has the expando 4872 id = target[expando]; 4873 if (id) { 4874 eventMap = events[id]; 4875 4876 // Specific callback 4877 if (names) { 4878 names = names.split(' '); 4879 i = names.length; 4880 while (i--) { 4881 name = names[i]; 4882 callbackList = eventMap[name]; 4883 4884 // Unbind the event if it exists in the map 4885 if (callbackList) { 4886 // Remove specified callback 4887 if (callback) { 4888 ci = callbackList.length; 4889 while (ci--) { 4890 if (callbackList[ci].func === callback) { 4891 callbackList.splice(ci, 1); 4892 } 4893 } 4894 } 4895 4896 // Remove all callbacks if there isn't a specified callback or there is no callbacks left 4897 if (!callback || callbackList.length === 0) { 4898 delete eventMap[name]; 4899 removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture); 4900 } 4901 } 4902 } 4903 } else { 4904 // All events for a specific element 4905 for (name in eventMap) { 4906 callbackList = eventMap[name]; 4907 removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture); 4908 } 4909 4910 eventMap = {}; 4911 } 4912 4913 // Check if object is empty, if it isn't then we won't remove the expando map 4914 for (name in eventMap) { 4915 return self; 4916 } 4917 4918 // Delete event object 4919 delete events[id]; 4920 4921 // Remove expando from target 4922 try { 4923 // IE will fail here since it can't delete properties from window 4924 delete target[expando]; 4925 } catch (ex) { 4926 // IE will set it to null 4927 target[expando] = null; 4928 } 4929 } 4930 4931 return self; 4932 }; 4933 4934 self.fire = function(target, name, args) { 4935 var id, event; 4936 4937 // Don't bind to text nodes or comments 4938 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4939 return self; 4940 } 4941 4942 // Build event object by patching the args 4943 event = fix(null, args); 4944 event.type = name; 4945 4946 do { 4947 // Found an expando that means there is listeners to execute 4948 id = target[expando]; 4949 if (id) { 4950 executeHandlers(event, id); 4951 } 4952 4953 // Walk up the DOM 4954 target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; 4955 } while (target && !event.isPropagationStopped()); 4956 4957 return self; 4958 }; 4959 4960 self.clean = function(target) { 4961 var i, children, unbind = self.unbind; 4962 4963 // Don't bind to text nodes or comments 4964 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4965 return self; 4966 } 4967 4968 // Unbind any element on the specificed target 4969 if (target[expando]) { 4970 unbind(target); 4971 } 4972 4973 // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children 4974 if (!target.getElementsByTagName) { 4975 target = target.document; 4976 } 4977 4978 // Remove events from each child element 4979 if (target && target.getElementsByTagName) { 4980 unbind(target); 4981 4982 children = target.getElementsByTagName('*'); 4983 i = children.length; 4984 while (i--) { 4985 target = children[i]; 4986 4987 if (target[expando]) { 4988 unbind(target); 4989 } 4990 } 4991 } 4992 4993 return self; 4994 }; 4995 4996 self.callNativeHandler = function(id, evt) { 4997 if (events) { 4998 events[id][evt.type].nativeHandler(evt); 4999 } 5000 }; 5001 5002 self.destory = function() { 5003 events = {}; 5004 }; 5005 5006 // Legacy function calls 5007 5008 self.add = function(target, events, func, scope) { 5009 // Old API supported direct ID assignment 5010 if (typeof(target) === "string") { 5011 target = document.getElementById(target); 5012 } 5013 5014 // Old API supported multiple targets 5015 if (target && target instanceof Array) { 5016 var i = target.length; 5017 5018 while (i--) { 5019 self.add(target[i], events, func, scope); 5020 } 5021 5022 return; 5023 } 5024 5025 // Old API called ready init 5026 if (events === "init") { 5027 events = "ready"; 5028 } 5029 5030 return self.bind(target, events instanceof Array ? events.join(' ') : events, func, scope); 5031 }; 5032 5033 self.remove = function(target, events, func, scope) { 5034 if (!target) { 5035 return self; 5036 } 5037 5038 // Old API supported direct ID assignment 5039 if (typeof(target) === "string") { 5040 target = document.getElementById(target); 5041 } 5042 5043 // Old API supported multiple targets 5044 if (target instanceof Array) { 5045 var i = target.length; 5046 5047 while (i--) { 5048 self.remove(target[i], events, func, scope); 5049 } 5050 5051 return self; 5052 } 5053 5054 return self.unbind(target, events instanceof Array ? events.join(' ') : events, func); 5055 }; 5056 5057 self.clear = function(target) { 5058 // Old API supported direct ID assignment 5059 if (typeof(target) === "string") { 5060 target = document.getElementById(target); 5061 } 5062 5063 return self.clean(target); 5064 }; 5065 5066 self.cancel = function(e) { 5067 if (e) { 5068 self.prevent(e); 5069 self.stop(e); 5070 } 5071 5072 return false; 5073 }; 5074 5075 self.prevent = function(e) { 5076 if (!e.preventDefault) { 5077 e = fix(e); 5078 } 5079 5080 e.preventDefault(); 5081 5082 return false; 5083 }; 5084 5085 self.stop = function(e) { 5086 if (!e.stopPropagation) { 5087 e = fix(e); 5088 } 5089 5090 e.stopPropagation(); 5091 5092 return false; 5093 }; 5094 } 5095 5096 namespace.EventUtils = EventUtils; 5097 5098 namespace.Event = new EventUtils(function(id) { 5099 return function(evt) { 5100 tinymce.dom.Event.callNativeHandler(id, evt); 5101 }; 5102 }); 5103 5104 // Bind ready event when tinymce script is loaded 5105 namespace.Event.bind(window, 'ready', function() {}); 5106 5107 namespace = 0; 5108 })(tinymce.dom, 'data-mce-expando'); // Namespace and expando 5109 5110 tinymce.dom.TreeWalker = function(start_node, root_node) { 5111 var node = start_node; 5112 5113 function findSibling(node, start_name, sibling_name, shallow) { 5114 var sibling, parent; 5115 5116 if (node) { 5117 // Walk into nodes if it has a start 5118 if (!shallow && node[start_name]) 5119 return node[start_name]; 5120 5121 // Return the sibling if it has one 5122 if (node != root_node) { 5123 sibling = node[sibling_name]; 5124 if (sibling) 5125 return sibling; 5126 5127 // Walk up the parents to look for siblings 5128 for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) { 5129 sibling = parent[sibling_name]; 5130 if (sibling) 5131 return sibling; 5132 } 5133 } 5134 } 5135 }; 5136 5137 this.current = function() { 5138 return node; 5139 }; 5140 5141 this.next = function(shallow) { 5142 return (node = findSibling(node, 'firstChild', 'nextSibling', shallow)); 5143 }; 5144 5145 this.prev = function(shallow) { 5146 return (node = findSibling(node, 'lastChild', 'previousSibling', shallow)); 5147 }; 5148 }; 5149 5150 (function(tinymce) { 5151 // Shorten names 5152 var each = tinymce.each, 5153 is = tinymce.is, 5154 isWebKit = tinymce.isWebKit, 5155 isIE = tinymce.isIE, 5156 Entities = tinymce.html.Entities, 5157 simpleSelectorRe = /^([a-z0-9],?)+$/i, 5158 whiteSpaceRegExp = /^[ \t\r\n]*$/; 5159 5160 tinymce.create('tinymce.dom.DOMUtils', { 5161 doc : null, 5162 root : null, 5163 files : null, 5164 pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/, 5165 props : { 5166 "for" : "htmlFor", 5167 "class" : "className", 5168 className : "className", 5169 checked : "checked", 5170 disabled : "disabled", 5171 maxlength : "maxLength", 5172 readonly : "readOnly", 5173 selected : "selected", 5174 value : "value", 5175 id : "id", 5176 name : "name", 5177 type : "type" 5178 }, 5179 5180 DOMUtils : function(d, s) { 5181 var t = this, globalStyle, name, blockElementsMap; 5182 5183 t.doc = d; 5184 t.win = window; 5185 t.files = {}; 5186 t.cssFlicker = false; 5187 t.counter = 0; 5188 t.stdMode = !tinymce.isIE || d.documentMode >= 8; 5189 t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode; 5190 t.hasOuterHTML = "outerHTML" in d.createElement("a"); 5191 5192 t.settings = s = tinymce.extend({ 5193 keep_values : false, 5194 hex_colors : 1 5195 }, s); 5196 5197 t.schema = s.schema; 5198 t.styles = new tinymce.html.Styles({ 5199 url_converter : s.url_converter, 5200 url_converter_scope : s.url_converter_scope 5201 }, s.schema); 5202 5203 // Fix IE6SP2 flicker and check it failed for pre SP2 5204 if (tinymce.isIE6) { 5205 try { 5206 d.execCommand('BackgroundImageCache', false, true); 5207 } catch (e) { 5208 t.cssFlicker = true; 5209 } 5210 } 5211 5212 t.fixDoc(d); 5213 t.events = s.ownEvents ? new tinymce.dom.EventUtils(s.proxy) : tinymce.dom.Event; 5214 tinymce.addUnload(t.destroy, t); 5215 blockElementsMap = s.schema ? s.schema.getBlockElements() : {}; 5216 5217 t.isBlock = function(node) { 5218 // This function is called in module pattern style since it might be executed with the wrong this scope 5219 var type = node.nodeType; 5220 5221 // If it's a node then check the type and use the nodeName 5222 if (type) 5223 return !!(type === 1 && blockElementsMap[node.nodeName]); 5224 5225 return !!blockElementsMap[node]; 5226 }; 5227 }, 5228 5229 fixDoc: function(doc) { 5230 var settings = this.settings, name; 5231 5232 if (isIE && settings.schema) { 5233 // Add missing HTML 4/5 elements to IE 5234 ('abbr article aside audio canvas ' + 5235 'details figcaption figure footer ' + 5236 'header hgroup mark menu meter nav ' + 5237 'output progress section summary ' + 5238 'time video').replace(/\w+/g, function(name) { 5239 doc.createElement(name); 5240 }); 5241 5242 // Create all custom elements 5243 for (name in settings.schema.getCustomElements()) { 5244 doc.createElement(name); 5245 } 5246 } 5247 }, 5248 5249 clone: function(node, deep) { 5250 var self = this, clone, doc; 5251 5252 // TODO: Add feature detection here in the future 5253 if (!isIE || node.nodeType !== 1 || deep) { 5254 return node.cloneNode(deep); 5255 } 5256 5257 doc = self.doc; 5258 5259 // Make a HTML5 safe shallow copy 5260 if (!deep) { 5261 clone = doc.createElement(node.nodeName); 5262 5263 // Copy attribs 5264 each(self.getAttribs(node), function(attr) { 5265 self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); 5266 }); 5267 5268 return clone; 5269 } 5270 /* 5271 // Setup HTML5 patched document fragment 5272 if (!self.frag) { 5273 self.frag = doc.createDocumentFragment(); 5274 self.fixDoc(self.frag); 5275 } 5276 5277 // Make a deep copy by adding it to the document fragment then removing it this removed the :section 5278 clone = doc.createElement('div'); 5279 self.frag.appendChild(clone); 5280 clone.innerHTML = node.outerHTML; 5281 self.frag.removeChild(clone); 5282 */ 5283 return clone.firstChild; 5284 }, 5285 5286 getRoot : function() { 5287 var t = this, s = t.settings; 5288 5289 return (s && t.get(s.root_element)) || t.doc.body; 5290 }, 5291 5292 getViewPort : function(w) { 5293 var d, b; 5294 5295 w = !w ? this.win : w; 5296 d = w.document; 5297 b = this.boxModel ? d.documentElement : d.body; 5298 5299 // Returns viewport size excluding scrollbars 5300 return { 5301 x : w.pageXOffset || b.scrollLeft, 5302 y : w.pageYOffset || b.scrollTop, 5303 w : w.innerWidth || b.clientWidth, 5304 h : w.innerHeight || b.clientHeight 5305 }; 5306 }, 5307 5308 getRect : function(e) { 5309 var p, t = this, sr; 5310 5311 e = t.get(e); 5312 p = t.getPos(e); 5313 sr = t.getSize(e); 5314 5315 return { 5316 x : p.x, 5317 y : p.y, 5318 w : sr.w, 5319 h : sr.h 5320 }; 5321 }, 5322 5323 getSize : function(e) { 5324 var t = this, w, h; 5325 5326 e = t.get(e); 5327 w = t.getStyle(e, 'width'); 5328 h = t.getStyle(e, 'height'); 5329 5330 // Non pixel value, then force offset/clientWidth 5331 if (w.indexOf('px') === -1) 5332 w = 0; 5333 5334 // Non pixel value, then force offset/clientWidth 5335 if (h.indexOf('px') === -1) 5336 h = 0; 5337 5338 return { 5339 w : parseInt(w, 10) || e.offsetWidth || e.clientWidth, 5340 h : parseInt(h, 10) || e.offsetHeight || e.clientHeight 5341 }; 5342 }, 5343 5344 getParent : function(n, f, r) { 5345 return this.getParents(n, f, r, false); 5346 }, 5347 5348 getParents : function(n, f, r, c) { 5349 var t = this, na, se = t.settings, o = []; 5350 5351 n = t.get(n); 5352 c = c === undefined; 5353 5354 if (se.strict_root) 5355 r = r || t.getRoot(); 5356 5357 // Wrap node name as func 5358 if (is(f, 'string')) { 5359 na = f; 5360 5361 if (f === '*') { 5362 f = function(n) {return n.nodeType == 1;}; 5363 } else { 5364 f = function(n) { 5365 return t.is(n, na); 5366 }; 5367 } 5368 } 5369 5370 while (n) { 5371 if (n == r || !n.nodeType || n.nodeType === 9) 5372 break; 5373 5374 if (!f || f(n)) { 5375 if (c) 5376 o.push(n); 5377 else 5378 return n; 5379 } 5380 5381 n = n.parentNode; 5382 } 5383 5384 return c ? o : null; 5385 }, 5386 5387 get : function(e) { 5388 var n; 5389 5390 if (e && this.doc && typeof(e) == 'string') { 5391 n = e; 5392 e = this.doc.getElementById(e); 5393 5394 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick 5395 if (e && e.id !== n) 5396 return this.doc.getElementsByName(n)[1]; 5397 } 5398 5399 return e; 5400 }, 5401 5402 getNext : function(node, selector) { 5403 return this._findSib(node, selector, 'nextSibling'); 5404 }, 5405 5406 getPrev : function(node, selector) { 5407 return this._findSib(node, selector, 'previousSibling'); 5408 }, 5409 5410 5411 add : function(p, n, a, h, c) { 5412 var t = this; 5413 5414 return this.run(p, function(p) { 5415 var e, k; 5416 5417 e = is(n, 'string') ? t.doc.createElement(n) : n; 5418 t.setAttribs(e, a); 5419 5420 if (h) { 5421 if (h.nodeType) 5422 e.appendChild(h); 5423 else 5424 t.setHTML(e, h); 5425 } 5426 5427 return !c ? p.appendChild(e) : e; 5428 }); 5429 }, 5430 5431 create : function(n, a, h) { 5432 return this.add(this.doc.createElement(n), n, a, h, 1); 5433 }, 5434 5435 createHTML : function(n, a, h) { 5436 var o = '', t = this, k; 5437 5438 o += '<' + n; 5439 5440 for (k in a) { 5441 if (a.hasOwnProperty(k)) 5442 o += ' ' + k + '="' + t.encode(a[k]) + '"'; 5443 } 5444 5445 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime 5446 if (typeof(h) != "undefined") 5447 return o + '>' + h + '</' + n + '>'; 5448 5449 return o + ' />'; 5450 }, 5451 5452 remove : function(node, keep_children) { 5453 return this.run(node, function(node) { 5454 var child, parent = node.parentNode; 5455 5456 if (!parent) 5457 return null; 5458 5459 if (keep_children) { 5460 while (child = node.firstChild) { 5461 // IE 8 will crash if you don't remove completely empty text nodes 5462 if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue) 5463 parent.insertBefore(child, node); 5464 else 5465 node.removeChild(child); 5466 } 5467 } 5468 5469 return parent.removeChild(node); 5470 }); 5471 }, 5472 5473 setStyle : function(n, na, v) { 5474 var t = this; 5475 5476 return t.run(n, function(e) { 5477 var s, i; 5478 5479 s = e.style; 5480 5481 // Camelcase it, if needed 5482 na = na.replace(/-(\D)/g, function(a, b){ 5483 return b.toUpperCase(); 5484 }); 5485 5486 // Default px suffix on these 5487 if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) 5488 v += 'px'; 5489 5490 switch (na) { 5491 case 'opacity': 5492 // IE specific opacity 5493 if (isIE) { 5494 s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; 5495 5496 if (!n.currentStyle || !n.currentStyle.hasLayout) 5497 s.display = 'inline-block'; 5498 } 5499 5500 // Fix for older browsers 5501 s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; 5502 break; 5503 5504 case 'float': 5505 isIE ? s.styleFloat = v : s.cssFloat = v; 5506 break; 5507 5508 default: 5509 s[na] = v || ''; 5510 } 5511 5512 // Force update of the style data 5513 if (t.settings.update_styles) 5514 t.setAttrib(e, 'data-mce-style'); 5515 }); 5516 }, 5517 5518 getStyle : function(n, na, c) { 5519 n = this.get(n); 5520 5521 if (!n) 5522 return; 5523 5524 // Gecko 5525 if (this.doc.defaultView && c) { 5526 // Remove camelcase 5527 na = na.replace(/[A-Z]/g, function(a){ 5528 return '-' + a; 5529 }); 5530 5531 try { 5532 return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); 5533 } catch (ex) { 5534 // Old safari might fail 5535 return null; 5536 } 5537 } 5538 5539 // Camelcase it, if needed 5540 na = na.replace(/-(\D)/g, function(a, b){ 5541 return b.toUpperCase(); 5542 }); 5543 5544 if (na == 'float') 5545 na = isIE ? 'styleFloat' : 'cssFloat'; 5546 5547 // IE & Opera 5548 if (n.currentStyle && c) 5549 return n.currentStyle[na]; 5550 5551 return n.style ? n.style[na] : undefined; 5552 }, 5553 5554 setStyles : function(e, o) { 5555 var t = this, s = t.settings, ol; 5556 5557 ol = s.update_styles; 5558 s.update_styles = 0; 5559 5560 each(o, function(v, n) { 5561 t.setStyle(e, n, v); 5562 }); 5563 5564 // Update style info 5565 s.update_styles = ol; 5566 if (s.update_styles) 5567 t.setAttrib(e, s.cssText); 5568 }, 5569 5570 removeAllAttribs: function(e) { 5571 return this.run(e, function(e) { 5572 var i, attrs = e.attributes; 5573 for (i = attrs.length - 1; i >= 0; i--) { 5574 e.removeAttributeNode(attrs.item(i)); 5575 } 5576 }); 5577 }, 5578 5579 setAttrib : function(e, n, v) { 5580 var t = this; 5581 5582 // Whats the point 5583 if (!e || !n) 5584 return; 5585 5586 // Strict XML mode 5587 if (t.settings.strict) 5588 n = n.toLowerCase(); 5589 5590 return this.run(e, function(e) { 5591 var s = t.settings; 5592 var originalValue = e.getAttribute(n); 5593 if (v !== null) { 5594 switch (n) { 5595 case "style": 5596 if (!is(v, 'string')) { 5597 each(v, function(v, n) { 5598 t.setStyle(e, n, v); 5599 }); 5600 5601 return; 5602 } 5603 5604 // No mce_style for elements with these since they might get resized by the user 5605 if (s.keep_values) { 5606 if (v && !t._isRes(v)) 5607 e.setAttribute('data-mce-style', v, 2); 5608 else 5609 e.removeAttribute('data-mce-style', 2); 5610 } 5611 5612 e.style.cssText = v; 5613 break; 5614 5615 case "class": 5616 e.className = v || ''; // Fix IE null bug 5617 break; 5618 5619 case "src": 5620 case "href": 5621 if (s.keep_values) { 5622 if (s.url_converter) 5623 v = s.url_converter.call(s.url_converter_scope || t, v, n, e); 5624 5625 t.setAttrib(e, 'data-mce-' + n, v, 2); 5626 } 5627 5628 break; 5629 5630 case "shape": 5631 e.setAttribute('data-mce-style', v); 5632 break; 5633 } 5634 } 5635 if (is(v) && v !== null && v.length !== 0) 5636 e.setAttribute(n, '' + v, 2); 5637 else 5638 e.removeAttribute(n, 2); 5639 5640 // fire onChangeAttrib event for attributes that have changed 5641 if (tinyMCE.activeEditor && originalValue != v) { 5642 var ed = tinyMCE.activeEditor; 5643 ed.onSetAttrib.dispatch(ed, e, n, v); 5644 } 5645 }); 5646 }, 5647 5648 setAttribs : function(e, o) { 5649 var t = this; 5650 5651 return this.run(e, function(e) { 5652 each(o, function(v, n) { 5653 t.setAttrib(e, n, v); 5654 }); 5655 }); 5656 }, 5657 5658 getAttrib : function(e, n, dv) { 5659 var v, t = this, undef; 5660 5661 e = t.get(e); 5662 5663 if (!e || e.nodeType !== 1) 5664 return dv === undef ? false : dv; 5665 5666 if (!is(dv)) 5667 dv = ''; 5668 5669 // Try the mce variant for these 5670 if (/^(src|href|style|coords|shape)$/.test(n)) { 5671 v = e.getAttribute("data-mce-" + n); 5672 5673 if (v) 5674 return v; 5675 } 5676 5677 if (isIE && t.props[n]) { 5678 v = e[t.props[n]]; 5679 v = v && v.nodeValue ? v.nodeValue : v; 5680 } 5681 5682 if (!v) 5683 v = e.getAttribute(n, 2); 5684 5685 // Check boolean attribs 5686 if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) { 5687 if (e[t.props[n]] === true && v === '') 5688 return n; 5689 5690 return v ? n : ''; 5691 } 5692 5693 // Inner input elements will override attributes on form elements 5694 if (e.nodeName === "FORM" && e.getAttributeNode(n)) 5695 return e.getAttributeNode(n).nodeValue; 5696 5697 if (n === 'style') { 5698 v = v || e.style.cssText; 5699 5700 if (v) { 5701 v = t.serializeStyle(t.parseStyle(v), e.nodeName); 5702 5703 if (t.settings.keep_values && !t._isRes(v)) 5704 e.setAttribute('data-mce-style', v); 5705 } 5706 } 5707 5708 // Remove Apple and WebKit stuff 5709 if (isWebKit && n === "class" && v) 5710 v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); 5711 5712 // Handle IE issues 5713 if (isIE) { 5714 switch (n) { 5715 case 'rowspan': 5716 case 'colspan': 5717 // IE returns 1 as default value 5718 if (v === 1) 5719 v = ''; 5720 5721 break; 5722 5723 case 'size': 5724 // IE returns +0 as default value for size 5725 if (v === '+0' || v === 20 || v === 0) 5726 v = ''; 5727 5728 break; 5729 5730 case 'width': 5731 case 'height': 5732 case 'vspace': 5733 case 'checked': 5734 case 'disabled': 5735 case 'readonly': 5736 if (v === 0) 5737 v = ''; 5738 5739 break; 5740 5741 case 'hspace': 5742 // IE returns -1 as default value 5743 if (v === -1) 5744 v = ''; 5745 5746 break; 5747 5748 case 'maxlength': 5749 case 'tabindex': 5750 // IE returns default value 5751 if (v === 32768 || v === 2147483647 || v === '32768') 5752 v = ''; 5753 5754 break; 5755 5756 case 'multiple': 5757 case 'compact': 5758 case 'noshade': 5759 case 'nowrap': 5760 if (v === 65535) 5761 return n; 5762 5763 return dv; 5764 5765 case 'shape': 5766 v = v.toLowerCase(); 5767 break; 5768 5769 default: 5770 // IE has odd anonymous function for event attributes 5771 if (n.indexOf('on') === 0 && v) 5772 v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v); 5773 } 5774 } 5775 5776 return (v !== undef && v !== null && v !== '') ? '' + v : dv; 5777 }, 5778 5779 getPos : function(n, ro) { 5780 var t = this, x = 0, y = 0, e, d = t.doc, r; 5781 5782 n = t.get(n); 5783 ro = ro || d.body; 5784 5785 if (n) { 5786 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes 5787 if (n.getBoundingClientRect) { 5788 n = n.getBoundingClientRect(); 5789 e = t.boxModel ? d.documentElement : d.body; 5790 5791 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit 5792 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position 5793 x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop; 5794 y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft; 5795 5796 return {x : x, y : y}; 5797 } 5798 5799 r = n; 5800 while (r && r != ro && r.nodeType) { 5801 x += r.offsetLeft || 0; 5802 y += r.offsetTop || 0; 5803 r = r.offsetParent; 5804 } 5805 5806 r = n.parentNode; 5807 while (r && r != ro && r.nodeType) { 5808 x -= r.scrollLeft || 0; 5809 y -= r.scrollTop || 0; 5810 r = r.parentNode; 5811 } 5812 } 5813 5814 return {x : x, y : y}; 5815 }, 5816 5817 parseStyle : function(st) { 5818 return this.styles.parse(st); 5819 }, 5820 5821 serializeStyle : function(o, name) { 5822 return this.styles.serialize(o, name); 5823 }, 5824 5825 addStyle: function(cssText) { 5826 var doc = this.doc, head; 5827 5828 // Create style element if needed 5829 styleElm = doc.getElementById('mceDefaultStyles'); 5830 if (!styleElm) { 5831 styleElm = doc.createElement('style'), 5832 styleElm.id = 'mceDefaultStyles'; 5833 styleElm.type = 'text/css'; 5834 5835 head = doc.getElementsByTagName('head')[0] 5836 if (head.firstChild) { 5837 head.insertBefore(styleElm, head.firstChild); 5838 } else { 5839 head.appendChild(styleElm); 5840 } 5841 } 5842 5843 // Append style data to old or new style element 5844 if (styleElm.styleSheet) { 5845 styleElm.styleSheet.cssText += cssText; 5846 } else { 5847 styleElm.appendChild(doc.createTextNode(cssText)); 5848 } 5849 }, 5850 5851 loadCSS : function(u) { 5852 var t = this, d = t.doc, head; 5853 5854 if (!u) 5855 u = ''; 5856 5857 head = d.getElementsByTagName('head')[0]; 5858 5859 each(u.split(','), function(u) { 5860 var link; 5861 5862 if (t.files[u]) 5863 return; 5864 5865 t.files[u] = true; 5866 link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); 5867 5868 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug 5869 // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading 5870 // It's ugly but it seems to work fine. 5871 if (isIE && d.documentMode && d.recalc) { 5872 link.onload = function() { 5873 if (d.recalc) 5874 d.recalc(); 5875 5876 link.onload = null; 5877 }; 5878 } 5879 5880 head.appendChild(link); 5881 }); 5882 }, 5883 5884 addClass : function(e, c) { 5885 return this.run(e, function(e) { 5886 var o; 5887 5888 if (!c) 5889 return 0; 5890 5891 if (this.hasClass(e, c)) 5892 return e.className; 5893 5894 o = this.removeClass(e, c); 5895 5896 return e.className = (o != '' ? (o + ' ') : '') + c; 5897 }); 5898 }, 5899 5900 removeClass : function(e, c) { 5901 var t = this, re; 5902 5903 return t.run(e, function(e) { 5904 var v; 5905 5906 if (t.hasClass(e, c)) { 5907 if (!re) 5908 re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); 5909 5910 v = e.className.replace(re, ' '); 5911 v = tinymce.trim(v != ' ' ? v : ''); 5912 5913 e.className = v; 5914 5915 // Empty class attr 5916 if (!v) { 5917 e.removeAttribute('class'); 5918 e.removeAttribute('className'); 5919 } 5920 5921 return v; 5922 } 5923 5924 return e.className; 5925 }); 5926 }, 5927 5928 hasClass : function(n, c) { 5929 n = this.get(n); 5930 5931 if (!n || !c) 5932 return false; 5933 5934 return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; 5935 }, 5936 5937 show : function(e) { 5938 return this.setStyle(e, 'display', 'block'); 5939 }, 5940 5941 hide : function(e) { 5942 return this.setStyle(e, 'display', 'none'); 5943 }, 5944 5945 isHidden : function(e) { 5946 e = this.get(e); 5947 5948 return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; 5949 }, 5950 5951 uniqueId : function(p) { 5952 return (!p ? 'mce_' : p) + (this.counter++); 5953 }, 5954 5955 setHTML : function(element, html) { 5956 var self = this; 5957 5958 return self.run(element, function(element) { 5959 if (isIE) { 5960 // Remove all child nodes, IE keeps empty text nodes in DOM 5961 while (element.firstChild) 5962 element.removeChild(element.firstChild); 5963 5964 try { 5965 // IE will remove comments from the beginning 5966 // unless you padd the contents with something 5967 element.innerHTML = '<br />' + html; 5968 element.removeChild(element.firstChild); 5969 } catch (ex) { 5970 // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p 5971 // This seems to fix this problem 5972 5973 // Create new div with HTML contents and a BR infront to keep comments 5974 var newElement = self.create('div'); 5975 newElement.innerHTML = '<br />' + html; 5976 5977 // Add all children from div to target 5978 each (tinymce.grep(newElement.childNodes), function(node, i) { 5979 // Skip br element 5980 if (i && element.canHaveHTML) 5981 element.appendChild(node); 5982 }); 5983 } 5984 } else 5985 element.innerHTML = html; 5986 5987 return html; 5988 }); 5989 }, 5990 5991 getOuterHTML : function(elm) { 5992 var doc, self = this; 5993 5994 elm = self.get(elm); 5995 5996 if (!elm) 5997 return null; 5998 5999 if (elm.nodeType === 1 && self.hasOuterHTML) 6000 return elm.outerHTML; 6001 6002 doc = (elm.ownerDocument || self.doc).createElement("body"); 6003 doc.appendChild(elm.cloneNode(true)); 6004 6005 return doc.innerHTML; 6006 }, 6007 6008 setOuterHTML : function(e, h, d) { 6009 var t = this; 6010 6011 function setHTML(e, h, d) { 6012 var n, tp; 6013 6014 tp = d.createElement("body"); 6015 tp.innerHTML = h; 6016 6017 n = tp.lastChild; 6018 while (n) { 6019 t.insertAfter(n.cloneNode(true), e); 6020 n = n.previousSibling; 6021 } 6022 6023 t.remove(e); 6024 }; 6025 6026 return this.run(e, function(e) { 6027 e = t.get(e); 6028 6029 // Only set HTML on elements 6030 if (e.nodeType == 1) { 6031 d = d || e.ownerDocument || t.doc; 6032 6033 if (isIE) { 6034 try { 6035 // Try outerHTML for IE it sometimes produces an unknown runtime error 6036 if (isIE && e.nodeType == 1) 6037 e.outerHTML = h; 6038 else 6039 setHTML(e, h, d); 6040 } catch (ex) { 6041 // Fix for unknown runtime error 6042 setHTML(e, h, d); 6043 } 6044 } else 6045 setHTML(e, h, d); 6046 } 6047 }); 6048 }, 6049 6050 decode : Entities.decode, 6051 6052 encode : Entities.encodeAllRaw, 6053 6054 insertAfter : function(node, reference_node) { 6055 reference_node = this.get(reference_node); 6056 6057 return this.run(node, function(node) { 6058 var parent, nextSibling; 6059 6060 parent = reference_node.parentNode; 6061 nextSibling = reference_node.nextSibling; 6062 6063 if (nextSibling) 6064 parent.insertBefore(node, nextSibling); 6065 else 6066 parent.appendChild(node); 6067 6068 return node; 6069 }); 6070 }, 6071 6072 replace : function(n, o, k) { 6073 var t = this; 6074 6075 if (is(o, 'array')) 6076 n = n.cloneNode(true); 6077 6078 return t.run(o, function(o) { 6079 if (k) { 6080 each(tinymce.grep(o.childNodes), function(c) { 6081 n.appendChild(c); 6082 }); 6083 } 6084 6085 return o.parentNode.replaceChild(n, o); 6086 }); 6087 }, 6088 6089 rename : function(elm, name) { 6090 var t = this, newElm; 6091 6092 if (elm.nodeName != name.toUpperCase()) { 6093 // Rename block element 6094 newElm = t.create(name); 6095 6096 // Copy attribs to new block 6097 each(t.getAttribs(elm), function(attr_node) { 6098 t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); 6099 }); 6100 6101 // Replace block 6102 t.replace(newElm, elm, 1); 6103 } 6104 6105 return newElm || elm; 6106 }, 6107 6108 findCommonAncestor : function(a, b) { 6109 var ps = a, pe; 6110 6111 while (ps) { 6112 pe = b; 6113 6114 while (pe && ps != pe) 6115 pe = pe.parentNode; 6116 6117 if (ps == pe) 6118 break; 6119 6120 ps = ps.parentNode; 6121 } 6122 6123 if (!ps && a.ownerDocument) 6124 return a.ownerDocument.documentElement; 6125 6126 return ps; 6127 }, 6128 6129 toHex : function(s) { 6130 var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s); 6131 6132 function hex(s) { 6133 s = parseInt(s, 10).toString(16); 6134 6135 return s.length > 1 ? s : '0' + s; // 0 -> 00 6136 }; 6137 6138 if (c) { 6139 s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); 6140 6141 return s; 6142 } 6143 6144 return s; 6145 }, 6146 6147 getClasses : function() { 6148 var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; 6149 6150 if (t.classes) 6151 return t.classes; 6152 6153 function addClasses(s) { 6154 // IE style imports 6155 each(s.imports, function(r) { 6156 addClasses(r); 6157 }); 6158 6159 each(s.cssRules || s.rules, function(r) { 6160 // Real type or fake it on IE 6161 switch (r.type || 1) { 6162 // Rule 6163 case 1: 6164 if (r.selectorText) { 6165 each(r.selectorText.split(','), function(v) { 6166 v = v.replace(/^\s*|\s*$|^\s\./g, ""); 6167 6168 // Is internal or it doesn't contain a class 6169 if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) 6170 return; 6171 6172 // Remove everything but class name 6173 ov = v; 6174 v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v); 6175 6176 // Filter classes 6177 if (f && !(v = f(v, ov))) 6178 return; 6179 6180 if (!lo[v]) { 6181 cl.push({'class' : v}); 6182 lo[v] = 1; 6183 } 6184 }); 6185 } 6186 break; 6187 6188 // Import 6189 case 3: 6190 addClasses(r.styleSheet); 6191 break; 6192 } 6193 }); 6194 }; 6195 6196 try { 6197 each(t.doc.styleSheets, addClasses); 6198 } catch (ex) { 6199 // Ignore 6200 } 6201 6202 if (cl.length > 0) 6203 t.classes = cl; 6204 6205 return cl; 6206 }, 6207 6208 run : function(e, f, s) { 6209 var t = this, o; 6210 6211 if (t.doc && typeof(e) === 'string') 6212 e = t.get(e); 6213 6214 if (!e) 6215 return false; 6216 6217 s = s || this; 6218 if (!e.nodeType && (e.length || e.length === 0)) { 6219 o = []; 6220 6221 each(e, function(e, i) { 6222 if (e) { 6223 if (typeof(e) == 'string') 6224 e = t.doc.getElementById(e); 6225 6226 o.push(f.call(s, e, i)); 6227 } 6228 }); 6229 6230 return o; 6231 } 6232 6233 return f.call(s, e); 6234 }, 6235 6236 getAttribs : function(n) { 6237 var o; 6238 6239 n = this.get(n); 6240 6241 if (!n) 6242 return []; 6243 6244 if (isIE) { 6245 o = []; 6246 6247 // Object will throw exception in IE 6248 if (n.nodeName == 'OBJECT') 6249 return n.attributes; 6250 6251 // IE doesn't keep the selected attribute if you clone option elements 6252 if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected')) 6253 o.push({specified : 1, nodeName : 'selected'}); 6254 6255 // It's crazy that this is faster in IE but it's because it returns all attributes all the time 6256 n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) { 6257 o.push({specified : 1, nodeName : a}); 6258 }); 6259 6260 return o; 6261 } 6262 6263 return n.attributes; 6264 }, 6265 6266 isEmpty : function(node, elements) { 6267 var self = this, i, attributes, type, walker, name, brCount = 0; 6268 6269 node = node.firstChild; 6270 if (node) { 6271 walker = new tinymce.dom.TreeWalker(node, node.parentNode); 6272 elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; 6273 6274 do { 6275 type = node.nodeType; 6276 6277 if (type === 1) { 6278 // Ignore bogus elements 6279 if (node.getAttribute('data-mce-bogus')) 6280 continue; 6281 6282 // Keep empty elements like <img /> 6283 name = node.nodeName.toLowerCase(); 6284 if (elements && elements[name]) { 6285 // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p> 6286 if (name === 'br') { 6287 brCount++; 6288 continue; 6289 } 6290 6291 return false; 6292 } 6293 6294 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a> 6295 attributes = self.getAttribs(node); 6296 i = node.attributes.length; 6297 while (i--) { 6298 name = node.attributes[i].nodeName; 6299 if (name === "name" || name === 'data-mce-bookmark') 6300 return false; 6301 } 6302 } 6303 6304 // Keep comment nodes 6305 if (type == 8) 6306 return false; 6307 6308 // Keep non whitespace text nodes 6309 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) 6310 return false; 6311 } while (node = walker.next()); 6312 } 6313 6314 return brCount <= 1; 6315 }, 6316 6317 destroy : function(s) { 6318 var t = this; 6319 6320 t.win = t.doc = t.root = t.events = t.frag = null; 6321 6322 // Manual destroy then remove unload handler 6323 if (!s) 6324 tinymce.removeUnload(t.destroy); 6325 }, 6326 6327 createRng : function() { 6328 var d = this.doc; 6329 6330 return d.createRange ? d.createRange() : new tinymce.dom.Range(this); 6331 }, 6332 6333 nodeIndex : function(node, normalized) { 6334 var idx = 0, lastNodeType, lastNode, nodeType; 6335 6336 if (node) { 6337 for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { 6338 nodeType = node.nodeType; 6339 6340 // Normalize text nodes 6341 if (normalized && nodeType == 3) { 6342 if (nodeType == lastNodeType || !node.nodeValue.length) 6343 continue; 6344 } 6345 idx++; 6346 lastNodeType = nodeType; 6347 } 6348 } 6349 6350 return idx; 6351 }, 6352 6353 split : function(pe, e, re) { 6354 var t = this, r = t.createRng(), bef, aft, pa; 6355 6356 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense 6357 // but we don't want that in our code since it serves no purpose for the end user 6358 // For example if this is chopped: 6359 // <p>text 1<span><b>CHOP</b></span>text 2</p> 6360 // would produce: 6361 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p> 6362 // this function will then trim of empty edges and produce: 6363 // <p>text 1</p><b>CHOP</b><p>text 2</p> 6364 function trim(node) { 6365 var i, children = node.childNodes, type = node.nodeType; 6366 6367 function surroundedBySpans(node) { 6368 var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; 6369 var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; 6370 return previousIsSpan && nextIsSpan; 6371 } 6372 6373 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') 6374 return; 6375 6376 for (i = children.length - 1; i >= 0; i--) 6377 trim(children[i]); 6378 6379 if (type != 9) { 6380 // Keep non whitespace text nodes 6381 if (type == 3 && node.nodeValue.length > 0) { 6382 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>" 6383 // Also keep text nodes with only spaces if surrounded by spans. 6384 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b 6385 var trimmedLength = tinymce.trim(node.nodeValue).length; 6386 if (!t.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) 6387 return; 6388 } else if (type == 1) { 6389 // If the only child is a bookmark then move it up 6390 children = node.childNodes; 6391 if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark') 6392 node.parentNode.insertBefore(children[0], node); 6393 6394 // Keep non empty elements or img, hr etc 6395 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) 6396 return; 6397 } 6398 6399 t.remove(node); 6400 } 6401 6402 return node; 6403 }; 6404 6405 if (pe && e) { 6406 // Get before chunk 6407 r.setStart(pe.parentNode, t.nodeIndex(pe)); 6408 r.setEnd(e.parentNode, t.nodeIndex(e)); 6409 bef = r.extractContents(); 6410 6411 // Get after chunk 6412 r = t.createRng(); 6413 r.setStart(e.parentNode, t.nodeIndex(e) + 1); 6414 r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1); 6415 aft = r.extractContents(); 6416 6417 // Insert before chunk 6418 pa = pe.parentNode; 6419 pa.insertBefore(trim(bef), pe); 6420 6421 // Insert middle chunk 6422 if (re) 6423 pa.replaceChild(re, e); 6424 else 6425 pa.insertBefore(e, pe); 6426 6427 // Insert after chunk 6428 pa.insertBefore(trim(aft), pe); 6429 t.remove(pe); 6430 6431 return re || e; 6432 } 6433 }, 6434 6435 bind : function(target, name, func, scope) { 6436 return this.events.add(target, name, func, scope || this); 6437 }, 6438 6439 unbind : function(target, name, func) { 6440 return this.events.remove(target, name, func); 6441 }, 6442 6443 fire : function(target, name, evt) { 6444 return this.events.fire(target, name, evt); 6445 }, 6446 6447 // Returns the content editable state of a node 6448 getContentEditable: function(node) { 6449 var contentEditable; 6450 6451 // Check type 6452 if (node.nodeType != 1) { 6453 return null; 6454 } 6455 6456 // Check for fake content editable 6457 contentEditable = node.getAttribute("data-mce-contenteditable"); 6458 if (contentEditable && contentEditable !== "inherit") { 6459 return contentEditable; 6460 } 6461 6462 // Check for real content editable 6463 return node.contentEditable !== "inherit" ? node.contentEditable : null; 6464 }, 6465 6466 6467 _findSib : function(node, selector, name) { 6468 var t = this, f = selector; 6469 6470 if (node) { 6471 // If expression make a function of it using is 6472 if (is(f, 'string')) { 6473 f = function(node) { 6474 return t.is(node, selector); 6475 }; 6476 } 6477 6478 // Loop all siblings 6479 for (node = node[name]; node; node = node[name]) { 6480 if (f(node)) 6481 return node; 6482 } 6483 } 6484 6485 return null; 6486 }, 6487 6488 _isRes : function(c) { 6489 // Is live resizble element 6490 return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c); 6491 } 6492 6493 /* 6494 walk : function(n, f, s) { 6495 var d = this.doc, w; 6496 6497 if (d.createTreeWalker) { 6498 w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); 6499 6500 while ((n = w.nextNode()) != null) 6501 f.call(s || this, n); 6502 } else 6503 tinymce.walk(n, f, 'childNodes', s); 6504 } 6505 */ 6506 6507 /* 6508 toRGB : function(s) { 6509 var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); 6510 6511 if (c) { 6512 // #FFF -> #FFFFFF 6513 if (!is(c[3])) 6514 c[3] = c[2] = c[1]; 6515 6516 return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; 6517 } 6518 6519 return s; 6520 } 6521 */ 6522 }); 6523 6524 tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); 6525 })(tinymce); 6526 6527 (function(ns) { 6528 // Range constructor 6529 function Range(dom) { 6530 var t = this, 6531 doc = dom.doc, 6532 EXTRACT = 0, 6533 CLONE = 1, 6534 DELETE = 2, 6535 TRUE = true, 6536 FALSE = false, 6537 START_OFFSET = 'startOffset', 6538 START_CONTAINER = 'startContainer', 6539 END_CONTAINER = 'endContainer', 6540 END_OFFSET = 'endOffset', 6541 extend = tinymce.extend, 6542 nodeIndex = dom.nodeIndex; 6543 6544 extend(t, { 6545 // Inital states 6546 startContainer : doc, 6547 startOffset : 0, 6548 endContainer : doc, 6549 endOffset : 0, 6550 collapsed : TRUE, 6551 commonAncestorContainer : doc, 6552 6553 // Range constants 6554 START_TO_START : 0, 6555 START_TO_END : 1, 6556 END_TO_END : 2, 6557 END_TO_START : 3, 6558 6559 // Public methods 6560 setStart : setStart, 6561 setEnd : setEnd, 6562 setStartBefore : setStartBefore, 6563 setStartAfter : setStartAfter, 6564 setEndBefore : setEndBefore, 6565 setEndAfter : setEndAfter, 6566 collapse : collapse, 6567 selectNode : selectNode, 6568 selectNodeContents : selectNodeContents, 6569 compareBoundaryPoints : compareBoundaryPoints, 6570 deleteContents : deleteContents, 6571 extractContents : extractContents, 6572 cloneContents : cloneContents, 6573 insertNode : insertNode, 6574 surroundContents : surroundContents, 6575 cloneRange : cloneRange, 6576 toStringIE : toStringIE 6577 }); 6578 6579 function createDocumentFragment() { 6580 return doc.createDocumentFragment(); 6581 }; 6582 6583 function setStart(n, o) { 6584 _setEndPoint(TRUE, n, o); 6585 }; 6586 6587 function setEnd(n, o) { 6588 _setEndPoint(FALSE, n, o); 6589 }; 6590 6591 function setStartBefore(n) { 6592 setStart(n.parentNode, nodeIndex(n)); 6593 }; 6594 6595 function setStartAfter(n) { 6596 setStart(n.parentNode, nodeIndex(n) + 1); 6597 }; 6598 6599 function setEndBefore(n) { 6600 setEnd(n.parentNode, nodeIndex(n)); 6601 }; 6602 6603 function setEndAfter(n) { 6604 setEnd(n.parentNode, nodeIndex(n) + 1); 6605 }; 6606 6607 function collapse(ts) { 6608 if (ts) { 6609 t[END_CONTAINER] = t[START_CONTAINER]; 6610 t[END_OFFSET] = t[START_OFFSET]; 6611 } else { 6612 t[START_CONTAINER] = t[END_CONTAINER]; 6613 t[START_OFFSET] = t[END_OFFSET]; 6614 } 6615 6616 t.collapsed = TRUE; 6617 }; 6618 6619 function selectNode(n) { 6620 setStartBefore(n); 6621 setEndAfter(n); 6622 }; 6623 6624 function selectNodeContents(n) { 6625 setStart(n, 0); 6626 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); 6627 }; 6628 6629 function compareBoundaryPoints(h, r) { 6630 var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET], 6631 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; 6632 6633 // Check START_TO_START 6634 if (h === 0) 6635 return _compareBoundaryPoints(sc, so, rsc, rso); 6636 6637 // Check START_TO_END 6638 if (h === 1) 6639 return _compareBoundaryPoints(ec, eo, rsc, rso); 6640 6641 // Check END_TO_END 6642 if (h === 2) 6643 return _compareBoundaryPoints(ec, eo, rec, reo); 6644 6645 // Check END_TO_START 6646 if (h === 3) 6647 return _compareBoundaryPoints(sc, so, rec, reo); 6648 }; 6649 6650 function deleteContents() { 6651 _traverse(DELETE); 6652 }; 6653 6654 function extractContents() { 6655 return _traverse(EXTRACT); 6656 }; 6657 6658 function cloneContents() { 6659 return _traverse(CLONE); 6660 }; 6661 6662 function insertNode(n) { 6663 var startContainer = this[START_CONTAINER], 6664 startOffset = this[START_OFFSET], nn, o; 6665 6666 // Node is TEXT_NODE or CDATA 6667 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { 6668 if (!startOffset) { 6669 // At the start of text 6670 startContainer.parentNode.insertBefore(n, startContainer); 6671 } else if (startOffset >= startContainer.nodeValue.length) { 6672 // At the end of text 6673 dom.insertAfter(n, startContainer); 6674 } else { 6675 // Middle, need to split 6676 nn = startContainer.splitText(startOffset); 6677 startContainer.parentNode.insertBefore(n, nn); 6678 } 6679 } else { 6680 // Insert element node 6681 if (startContainer.childNodes.length > 0) 6682 o = startContainer.childNodes[startOffset]; 6683 6684 if (o) 6685 startContainer.insertBefore(n, o); 6686 else 6687 startContainer.appendChild(n); 6688 } 6689 }; 6690 6691 function surroundContents(n) { 6692 var f = t.extractContents(); 6693 6694 t.insertNode(n); 6695 n.appendChild(f); 6696 t.selectNode(n); 6697 }; 6698 6699 function cloneRange() { 6700 return extend(new Range(dom), { 6701 startContainer : t[START_CONTAINER], 6702 startOffset : t[START_OFFSET], 6703 endContainer : t[END_CONTAINER], 6704 endOffset : t[END_OFFSET], 6705 collapsed : t.collapsed, 6706 commonAncestorContainer : t.commonAncestorContainer 6707 }); 6708 }; 6709 6710 // Private methods 6711 6712 function _getSelectedNode(container, offset) { 6713 var child; 6714 6715 if (container.nodeType == 3 /* TEXT_NODE */) 6716 return container; 6717 6718 if (offset < 0) 6719 return container; 6720 6721 child = container.firstChild; 6722 while (child && offset > 0) { 6723 --offset; 6724 child = child.nextSibling; 6725 } 6726 6727 if (child) 6728 return child; 6729 6730 return container; 6731 }; 6732 6733 function _isCollapsed() { 6734 return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); 6735 }; 6736 6737 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { 6738 var c, offsetC, n, cmnRoot, childA, childB; 6739 6740 // In the first case the boundary-points have the same container. A is before B 6741 // if its offset is less than the offset of B, A is equal to B if its offset is 6742 // equal to the offset of B, and A is after B if its offset is greater than the 6743 // offset of B. 6744 if (containerA == containerB) { 6745 if (offsetA == offsetB) 6746 return 0; // equal 6747 6748 if (offsetA < offsetB) 6749 return -1; // before 6750 6751 return 1; // after 6752 } 6753 6754 // In the second case a child node C of the container of A is an ancestor 6755 // container of B. In this case, A is before B if the offset of A is less than or 6756 // equal to the index of the child node C and A is after B otherwise. 6757 c = containerB; 6758 while (c && c.parentNode != containerA) 6759 c = c.parentNode; 6760 6761 if (c) { 6762 offsetC = 0; 6763 n = containerA.firstChild; 6764 6765 while (n != c && offsetC < offsetA) { 6766 offsetC++; 6767 n = n.nextSibling; 6768 } 6769 6770 if (offsetA <= offsetC) 6771 return -1; // before 6772 6773 return 1; // after 6774 } 6775 6776 // In the third case a child node C of the container of B is an ancestor container 6777 // of A. In this case, A is before B if the index of the child node C is less than 6778 // the offset of B and A is after B otherwise. 6779 c = containerA; 6780 while (c && c.parentNode != containerB) { 6781 c = c.parentNode; 6782 } 6783 6784 if (c) { 6785 offsetC = 0; 6786 n = containerB.firstChild; 6787 6788 while (n != c && offsetC < offsetB) { 6789 offsetC++; 6790 n = n.nextSibling; 6791 } 6792 6793 if (offsetC < offsetB) 6794 return -1; // before 6795 6796 return 1; // after 6797 } 6798 6799 // In the fourth case, none of three other cases hold: the containers of A and B 6800 // are siblings or descendants of sibling nodes. In this case, A is before B if 6801 // the container of A is before the container of B in a pre-order traversal of the 6802 // Ranges' context tree and A is after B otherwise. 6803 cmnRoot = dom.findCommonAncestor(containerA, containerB); 6804 childA = containerA; 6805 6806 while (childA && childA.parentNode != cmnRoot) 6807 childA = childA.parentNode; 6808 6809 if (!childA) 6810 childA = cmnRoot; 6811 6812 childB = containerB; 6813 while (childB && childB.parentNode != cmnRoot) 6814 childB = childB.parentNode; 6815 6816 if (!childB) 6817 childB = cmnRoot; 6818 6819 if (childA == childB) 6820 return 0; // equal 6821 6822 n = cmnRoot.firstChild; 6823 while (n) { 6824 if (n == childA) 6825 return -1; // before 6826 6827 if (n == childB) 6828 return 1; // after 6829 6830 n = n.nextSibling; 6831 } 6832 }; 6833 6834 function _setEndPoint(st, n, o) { 6835 var ec, sc; 6836 6837 if (st) { 6838 t[START_CONTAINER] = n; 6839 t[START_OFFSET] = o; 6840 } else { 6841 t[END_CONTAINER] = n; 6842 t[END_OFFSET] = o; 6843 } 6844 6845 // If one boundary-point of a Range is set to have a root container 6846 // other than the current one for the Range, the Range is collapsed to 6847 // the new position. This enforces the restriction that both boundary- 6848 // points of a Range must have the same root container. 6849 ec = t[END_CONTAINER]; 6850 while (ec.parentNode) 6851 ec = ec.parentNode; 6852 6853 sc = t[START_CONTAINER]; 6854 while (sc.parentNode) 6855 sc = sc.parentNode; 6856 6857 if (sc == ec) { 6858 // The start position of a Range is guaranteed to never be after the 6859 // end position. To enforce this restriction, if the start is set to 6860 // be at a position after the end, the Range is collapsed to that 6861 // position. 6862 if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0) 6863 t.collapse(st); 6864 } else 6865 t.collapse(st); 6866 6867 t.collapsed = _isCollapsed(); 6868 t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]); 6869 }; 6870 6871 function _traverse(how) { 6872 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; 6873 6874 if (t[START_CONTAINER] == t[END_CONTAINER]) 6875 return _traverseSameContainer(how); 6876 6877 for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 6878 if (p == t[START_CONTAINER]) 6879 return _traverseCommonStartContainer(c, how); 6880 6881 ++endContainerDepth; 6882 } 6883 6884 for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 6885 if (p == t[END_CONTAINER]) 6886 return _traverseCommonEndContainer(c, how); 6887 6888 ++startContainerDepth; 6889 } 6890 6891 depthDiff = startContainerDepth - endContainerDepth; 6892 6893 startNode = t[START_CONTAINER]; 6894 while (depthDiff > 0) { 6895 startNode = startNode.parentNode; 6896 depthDiff--; 6897 } 6898 6899 endNode = t[END_CONTAINER]; 6900 while (depthDiff < 0) { 6901 endNode = endNode.parentNode; 6902 depthDiff++; 6903 } 6904 6905 // ascend the ancestor hierarchy until we have a common parent. 6906 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { 6907 startNode = sp; 6908 endNode = ep; 6909 } 6910 6911 return _traverseCommonAncestors(startNode, endNode, how); 6912 }; 6913 6914 function _traverseSameContainer(how) { 6915 var frag, s, sub, n, cnt, sibling, xferNode, start, len; 6916 6917 if (how != DELETE) 6918 frag = createDocumentFragment(); 6919 6920 // If selection is empty, just return the fragment 6921 if (t[START_OFFSET] == t[END_OFFSET]) 6922 return frag; 6923 6924 // Text node needs special case handling 6925 if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { 6926 // get the substring 6927 s = t[START_CONTAINER].nodeValue; 6928 sub = s.substring(t[START_OFFSET], t[END_OFFSET]); 6929 6930 // set the original text node to its new value 6931 if (how != CLONE) { 6932 n = t[START_CONTAINER]; 6933 start = t[START_OFFSET]; 6934 len = t[END_OFFSET] - t[START_OFFSET]; 6935 6936 if (start === 0 && len >= n.nodeValue.length - 1) { 6937 n.parentNode.removeChild(n); 6938 } else { 6939 n.deleteData(start, len); 6940 } 6941 6942 // Nothing is partially selected, so collapse to start point 6943 t.collapse(TRUE); 6944 } 6945 6946 if (how == DELETE) 6947 return; 6948 6949 if (sub.length > 0) { 6950 frag.appendChild(doc.createTextNode(sub)); 6951 } 6952 6953 return frag; 6954 } 6955 6956 // Copy nodes between the start/end offsets. 6957 n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); 6958 cnt = t[END_OFFSET] - t[START_OFFSET]; 6959 6960 while (n && cnt > 0) { 6961 sibling = n.nextSibling; 6962 xferNode = _traverseFullySelected(n, how); 6963 6964 if (frag) 6965 frag.appendChild( xferNode ); 6966 6967 --cnt; 6968 n = sibling; 6969 } 6970 6971 // Nothing is partially selected, so collapse to start point 6972 if (how != CLONE) 6973 t.collapse(TRUE); 6974 6975 return frag; 6976 }; 6977 6978 function _traverseCommonStartContainer(endAncestor, how) { 6979 var frag, n, endIdx, cnt, sibling, xferNode; 6980 6981 if (how != DELETE) 6982 frag = createDocumentFragment(); 6983 6984 n = _traverseRightBoundary(endAncestor, how); 6985 6986 if (frag) 6987 frag.appendChild(n); 6988 6989 endIdx = nodeIndex(endAncestor); 6990 cnt = endIdx - t[START_OFFSET]; 6991 6992 if (cnt <= 0) { 6993 // Collapse to just before the endAncestor, which 6994 // is partially selected. 6995 if (how != CLONE) { 6996 t.setEndBefore(endAncestor); 6997 t.collapse(FALSE); 6998 } 6999 7000 return frag; 7001 } 7002 7003 n = endAncestor.previousSibling; 7004 while (cnt > 0) { 7005 sibling = n.previousSibling; 7006 xferNode = _traverseFullySelected(n, how); 7007 7008 if (frag) 7009 frag.insertBefore(xferNode, frag.firstChild); 7010 7011 --cnt; 7012 n = sibling; 7013 } 7014 7015 // Collapse to just before the endAncestor, which 7016 // is partially selected. 7017 if (how != CLONE) { 7018 t.setEndBefore(endAncestor); 7019 t.collapse(FALSE); 7020 } 7021 7022 return frag; 7023 }; 7024 7025 function _traverseCommonEndContainer(startAncestor, how) { 7026 var frag, startIdx, n, cnt, sibling, xferNode; 7027 7028 if (how != DELETE) 7029 frag = createDocumentFragment(); 7030 7031 n = _traverseLeftBoundary(startAncestor, how); 7032 if (frag) 7033 frag.appendChild(n); 7034 7035 startIdx = nodeIndex(startAncestor); 7036 ++startIdx; // Because we already traversed it 7037 7038 cnt = t[END_OFFSET] - startIdx; 7039 n = startAncestor.nextSibling; 7040 while (n && cnt > 0) { 7041 sibling = n.nextSibling; 7042 xferNode = _traverseFullySelected(n, how); 7043 7044 if (frag) 7045 frag.appendChild(xferNode); 7046 7047 --cnt; 7048 n = sibling; 7049 } 7050 7051 if (how != CLONE) { 7052 t.setStartAfter(startAncestor); 7053 t.collapse(TRUE); 7054 } 7055 7056 return frag; 7057 }; 7058 7059 function _traverseCommonAncestors(startAncestor, endAncestor, how) { 7060 var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; 7061 7062 if (how != DELETE) 7063 frag = createDocumentFragment(); 7064 7065 n = _traverseLeftBoundary(startAncestor, how); 7066 if (frag) 7067 frag.appendChild(n); 7068 7069 commonParent = startAncestor.parentNode; 7070 startOffset = nodeIndex(startAncestor); 7071 endOffset = nodeIndex(endAncestor); 7072 ++startOffset; 7073 7074 cnt = endOffset - startOffset; 7075 sibling = startAncestor.nextSibling; 7076 7077 while (cnt > 0) { 7078 nextSibling = sibling.nextSibling; 7079 n = _traverseFullySelected(sibling, how); 7080 7081 if (frag) 7082 frag.appendChild(n); 7083 7084 sibling = nextSibling; 7085 --cnt; 7086 } 7087 7088 n = _traverseRightBoundary(endAncestor, how); 7089 7090 if (frag) 7091 frag.appendChild(n); 7092 7093 if (how != CLONE) { 7094 t.setStartAfter(startAncestor); 7095 t.collapse(TRUE); 7096 } 7097 7098 return frag; 7099 }; 7100 7101 function _traverseRightBoundary(root, how) { 7102 var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER]; 7103 7104 if (next == root) 7105 return _traverseNode(next, isFullySelected, FALSE, how); 7106 7107 parent = next.parentNode; 7108 clonedParent = _traverseNode(parent, FALSE, FALSE, how); 7109 7110 while (parent) { 7111 while (next) { 7112 prevSibling = next.previousSibling; 7113 clonedChild = _traverseNode(next, isFullySelected, FALSE, how); 7114 7115 if (how != DELETE) 7116 clonedParent.insertBefore(clonedChild, clonedParent.firstChild); 7117 7118 isFullySelected = TRUE; 7119 next = prevSibling; 7120 } 7121 7122 if (parent == root) 7123 return clonedParent; 7124 7125 next = parent.previousSibling; 7126 parent = parent.parentNode; 7127 7128 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); 7129 7130 if (how != DELETE) 7131 clonedGrandParent.appendChild(clonedParent); 7132 7133 clonedParent = clonedGrandParent; 7134 } 7135 }; 7136 7137 function _traverseLeftBoundary(root, how) { 7138 var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; 7139 7140 if (next == root) 7141 return _traverseNode(next, isFullySelected, TRUE, how); 7142 7143 parent = next.parentNode; 7144 clonedParent = _traverseNode(parent, FALSE, TRUE, how); 7145 7146 while (parent) { 7147 while (next) { 7148 nextSibling = next.nextSibling; 7149 clonedChild = _traverseNode(next, isFullySelected, TRUE, how); 7150 7151 if (how != DELETE) 7152 clonedParent.appendChild(clonedChild); 7153 7154 isFullySelected = TRUE; 7155 next = nextSibling; 7156 } 7157 7158 if (parent == root) 7159 return clonedParent; 7160 7161 next = parent.nextSibling; 7162 parent = parent.parentNode; 7163 7164 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); 7165 7166 if (how != DELETE) 7167 clonedGrandParent.appendChild(clonedParent); 7168 7169 clonedParent = clonedGrandParent; 7170 } 7171 }; 7172 7173 function _traverseNode(n, isFullySelected, isLeft, how) { 7174 var txtValue, newNodeValue, oldNodeValue, offset, newNode; 7175 7176 if (isFullySelected) 7177 return _traverseFullySelected(n, how); 7178 7179 if (n.nodeType == 3 /* TEXT_NODE */) { 7180 txtValue = n.nodeValue; 7181 7182 if (isLeft) { 7183 offset = t[START_OFFSET]; 7184 newNodeValue = txtValue.substring(offset); 7185 oldNodeValue = txtValue.substring(0, offset); 7186 } else { 7187 offset = t[END_OFFSET]; 7188 newNodeValue = txtValue.substring(0, offset); 7189 oldNodeValue = txtValue.substring(offset); 7190 } 7191 7192 if (how != CLONE) 7193 n.nodeValue = oldNodeValue; 7194 7195 if (how == DELETE) 7196 return; 7197 7198 newNode = dom.clone(n, FALSE); 7199 newNode.nodeValue = newNodeValue; 7200 7201 return newNode; 7202 } 7203 7204 if (how == DELETE) 7205 return; 7206 7207 return dom.clone(n, FALSE); 7208 }; 7209 7210 function _traverseFullySelected(n, how) { 7211 if (how != DELETE) 7212 return how == CLONE ? dom.clone(n, TRUE) : n; 7213 7214 n.parentNode.removeChild(n); 7215 }; 7216 7217 function toStringIE() { 7218 return dom.create('body', null, cloneContents()).outerText; 7219 } 7220 7221 return t; 7222 }; 7223 7224 ns.Range = Range; 7225 7226 // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype 7227 Range.prototype.toString = function() { 7228 return this.toStringIE(); 7229 }; 7230 })(tinymce.dom); 7231 7232 (function() { 7233 function Selection(selection) { 7234 var self = this, dom = selection.dom, TRUE = true, FALSE = false; 7235 7236 function getPosition(rng, start) { 7237 var checkRng, startIndex = 0, endIndex, inside, 7238 children, child, offset, index, position = -1, parent; 7239 7240 // Setup test range, collapse it and get the parent 7241 checkRng = rng.duplicate(); 7242 checkRng.collapse(start); 7243 parent = checkRng.parentElement(); 7244 7245 // Check if the selection is within the right document 7246 if (parent.ownerDocument !== selection.dom.doc) 7247 return; 7248 7249 // IE will report non editable elements as it's parent so look for an editable one 7250 while (parent.contentEditable === "false") { 7251 parent = parent.parentNode; 7252 } 7253 7254 // If parent doesn't have any children then return that we are inside the element 7255 if (!parent.hasChildNodes()) { 7256 return {node : parent, inside : 1}; 7257 } 7258 7259 // Setup node list and endIndex 7260 children = parent.children; 7261 endIndex = children.length - 1; 7262 7263 // Perform a binary search for the position 7264 while (startIndex <= endIndex) { 7265 index = Math.floor((startIndex + endIndex) / 2); 7266 7267 // Move selection to node and compare the ranges 7268 child = children[index]; 7269 checkRng.moveToElementText(child); 7270 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); 7271 7272 // Before/after or an exact match 7273 if (position > 0) { 7274 endIndex = index - 1; 7275 } else if (position < 0) { 7276 startIndex = index + 1; 7277 } else { 7278 return {node : child}; 7279 } 7280 } 7281 7282 // Check if child position is before or we didn't find a position 7283 if (position < 0) { 7284 // No element child was found use the parent element and the offset inside that 7285 if (!child) { 7286 checkRng.moveToElementText(parent); 7287 checkRng.collapse(true); 7288 child = parent; 7289 inside = true; 7290 } else 7291 checkRng.collapse(false); 7292 7293 // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one 7294 // We need to walk char by char since rng.text or rng.htmlText will trim line endings 7295 offset = 0; 7296 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 7297 if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { 7298 break; 7299 } 7300 7301 offset++; 7302 } 7303 } else { 7304 // Child position is after the selection endpoint 7305 checkRng.collapse(true); 7306 7307 // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one 7308 offset = 0; 7309 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 7310 if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { 7311 break; 7312 } 7313 7314 offset++; 7315 } 7316 } 7317 7318 return {node : child, position : position, offset : offset, inside : inside}; 7319 }; 7320 7321 // Returns a W3C DOM compatible range object by using the IE Range API 7322 function getRange() { 7323 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; 7324 7325 // If selection is outside the current document just return an empty range 7326 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); 7327 if (element.ownerDocument != dom.doc) 7328 return domRange; 7329 7330 collapsed = selection.isCollapsed(); 7331 7332 // Handle control selection 7333 if (ieRange.item) { 7334 domRange.setStart(element.parentNode, dom.nodeIndex(element)); 7335 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); 7336 7337 return domRange; 7338 } 7339 7340 function findEndPoint(start) { 7341 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; 7342 7343 container = endPoint.node; 7344 offset = endPoint.offset; 7345 7346 if (endPoint.inside && !container.hasChildNodes()) { 7347 domRange[start ? 'setStart' : 'setEnd'](container, 0); 7348 return; 7349 } 7350 7351 if (offset === undef) { 7352 domRange[start ? 'setStartBefore' : 'setEndAfter'](container); 7353 return; 7354 } 7355 7356 if (endPoint.position < 0) { 7357 sibling = endPoint.inside ? container.firstChild : container.nextSibling; 7358 7359 if (!sibling) { 7360 domRange[start ? 'setStartAfter' : 'setEndAfter'](container); 7361 return; 7362 } 7363 7364 if (!offset) { 7365 if (sibling.nodeType == 3) 7366 domRange[start ? 'setStart' : 'setEnd'](sibling, 0); 7367 else 7368 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); 7369 7370 return; 7371 } 7372 7373 // Find the text node and offset 7374 while (sibling) { 7375 nodeValue = sibling.nodeValue; 7376 textNodeOffset += nodeValue.length; 7377 7378 // We are at or passed the position we where looking for 7379 if (textNodeOffset >= offset) { 7380 container = sibling; 7381 textNodeOffset -= offset; 7382 textNodeOffset = nodeValue.length - textNodeOffset; 7383 break; 7384 } 7385 7386 sibling = sibling.nextSibling; 7387 } 7388 } else { 7389 // Find the text node and offset 7390 sibling = container.previousSibling; 7391 7392 if (!sibling) 7393 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); 7394 7395 // If there isn't any text to loop then use the first position 7396 if (!offset) { 7397 if (container.nodeType == 3) 7398 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); 7399 else 7400 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); 7401 7402 return; 7403 } 7404 7405 while (sibling) { 7406 textNodeOffset += sibling.nodeValue.length; 7407 7408 // We are at or passed the position we where looking for 7409 if (textNodeOffset >= offset) { 7410 container = sibling; 7411 textNodeOffset -= offset; 7412 break; 7413 } 7414 7415 sibling = sibling.previousSibling; 7416 } 7417 } 7418 7419 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); 7420 }; 7421 7422 try { 7423 // Find start point 7424 findEndPoint(true); 7425 7426 // Find end point if needed 7427 if (!collapsed) 7428 findEndPoint(); 7429 } catch (ex) { 7430 // IE has a nasty bug where text nodes might throw "invalid argument" when you 7431 // access the nodeValue or other properties of text nodes. This seems to happend when 7432 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. 7433 if (ex.number == -2147024809) { 7434 // Get the current selection 7435 bookmark = self.getBookmark(2); 7436 7437 // Get start element 7438 tmpRange = ieRange.duplicate(); 7439 tmpRange.collapse(true); 7440 element = tmpRange.parentElement(); 7441 7442 // Get end element 7443 if (!collapsed) { 7444 tmpRange = ieRange.duplicate(); 7445 tmpRange.collapse(false); 7446 element2 = tmpRange.parentElement(); 7447 element2.innerHTML = element2.innerHTML; 7448 } 7449 7450 // Remove the broken elements 7451 element.innerHTML = element.innerHTML; 7452 7453 // Restore the selection 7454 self.moveToBookmark(bookmark); 7455 7456 // Since the range has moved we need to re-get it 7457 ieRange = selection.getRng(); 7458 7459 // Find start point 7460 findEndPoint(true); 7461 7462 // Find end point if needed 7463 if (!collapsed) 7464 findEndPoint(); 7465 } else 7466 throw ex; // Throw other errors 7467 } 7468 7469 return domRange; 7470 }; 7471 7472 this.getBookmark = function(type) { 7473 var rng = selection.getRng(), start, end, bookmark = {}; 7474 7475 function getIndexes(node) { 7476 var parent, root, children, i, indexes = []; 7477 7478 parent = node.parentNode; 7479 root = dom.getRoot().parentNode; 7480 7481 while (parent != root && parent.nodeType !== 9) { 7482 children = parent.children; 7483 7484 i = children.length; 7485 while (i--) { 7486 if (node === children[i]) { 7487 indexes.push(i); 7488 break; 7489 } 7490 } 7491 7492 node = parent; 7493 parent = parent.parentNode; 7494 } 7495 7496 return indexes; 7497 }; 7498 7499 function getBookmarkEndPoint(start) { 7500 var position; 7501 7502 position = getPosition(rng, start); 7503 if (position) { 7504 return { 7505 position : position.position, 7506 offset : position.offset, 7507 indexes : getIndexes(position.node), 7508 inside : position.inside 7509 }; 7510 } 7511 }; 7512 7513 // Non ubstructive bookmark 7514 if (type === 2) { 7515 // Handle text selection 7516 if (!rng.item) { 7517 bookmark.start = getBookmarkEndPoint(true); 7518 7519 if (!selection.isCollapsed()) 7520 bookmark.end = getBookmarkEndPoint(); 7521 } else 7522 bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; 7523 } 7524 7525 return bookmark; 7526 }; 7527 7528 this.moveToBookmark = function(bookmark) { 7529 var rng, body = dom.doc.body; 7530 7531 function resolveIndexes(indexes) { 7532 var node, i, idx, children; 7533 7534 node = dom.getRoot(); 7535 for (i = indexes.length - 1; i >= 0; i--) { 7536 children = node.children; 7537 idx = indexes[i]; 7538 7539 if (idx <= children.length - 1) { 7540 node = children[idx]; 7541 } 7542 } 7543 7544 return node; 7545 }; 7546 7547 function setBookmarkEndPoint(start) { 7548 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; 7549 7550 if (endPoint) { 7551 moveLeft = endPoint.position > 0; 7552 7553 moveRng = body.createTextRange(); 7554 moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); 7555 7556 offset = endPoint.offset; 7557 if (offset !== undef) { 7558 moveRng.collapse(endPoint.inside || moveLeft); 7559 moveRng.moveStart('character', moveLeft ? -offset : offset); 7560 } else 7561 moveRng.collapse(start); 7562 7563 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); 7564 7565 if (start) 7566 rng.collapse(true); 7567 } 7568 }; 7569 7570 if (bookmark.start) { 7571 if (bookmark.start.ctrl) { 7572 rng = body.createControlRange(); 7573 rng.addElement(resolveIndexes(bookmark.start.indexes)); 7574 rng.select(); 7575 } else { 7576 rng = body.createTextRange(); 7577 setBookmarkEndPoint(true); 7578 setBookmarkEndPoint(); 7579 rng.select(); 7580 } 7581 } 7582 }; 7583 7584 this.addRange = function(rng) { 7585 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, doc = selection.dom.doc, body = doc.body; 7586 7587 function setEndPoint(start) { 7588 var container, offset, marker, tmpRng, nodes; 7589 7590 marker = dom.create('a'); 7591 container = start ? startContainer : endContainer; 7592 offset = start ? startOffset : endOffset; 7593 tmpRng = ieRng.duplicate(); 7594 7595 if (container == doc || container == doc.documentElement) { 7596 container = body; 7597 offset = 0; 7598 } 7599 7600 if (container.nodeType == 3) { 7601 container.parentNode.insertBefore(marker, container); 7602 tmpRng.moveToElementText(marker); 7603 tmpRng.moveStart('character', offset); 7604 dom.remove(marker); 7605 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 7606 } else { 7607 nodes = container.childNodes; 7608 7609 if (nodes.length) { 7610 if (offset >= nodes.length) { 7611 dom.insertAfter(marker, nodes[nodes.length - 1]); 7612 } else { 7613 container.insertBefore(marker, nodes[offset]); 7614 } 7615 7616 tmpRng.moveToElementText(marker); 7617 } else if (container.canHaveHTML) { 7618 // Empty node selection for example <div>|</div> 7619 // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open 7620 container.innerHTML = '<span>\uFEFF</span>'; 7621 marker = container.firstChild; 7622 tmpRng.moveToElementText(marker); 7623 tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason 7624 } 7625 7626 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 7627 dom.remove(marker); 7628 } 7629 } 7630 7631 // Setup some shorter versions 7632 startContainer = rng.startContainer; 7633 startOffset = rng.startOffset; 7634 endContainer = rng.endContainer; 7635 endOffset = rng.endOffset; 7636 ieRng = body.createTextRange(); 7637 7638 // If single element selection then try making a control selection out of it 7639 if (startContainer == endContainer && startContainer.nodeType == 1) { 7640 // Trick to place the caret inside an empty block element like <p></p> 7641 if (startOffset == endOffset && !startContainer.hasChildNodes()) { 7642 if (startContainer.canHaveHTML) { 7643 // Check if previous sibling is an empty block if it is then we need to render it 7644 // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 7645 // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p> 7646 sibling = startContainer.previousSibling; 7647 if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { 7648 sibling.innerHTML = '\uFEFF'; 7649 } else { 7650 sibling = null; 7651 } 7652 7653 startContainer.innerHTML = '<span>\uFEFF</span><span>\uFEFF</span>'; 7654 ieRng.moveToElementText(startContainer.lastChild); 7655 ieRng.select(); 7656 dom.doc.selection.clear(); 7657 startContainer.innerHTML = ''; 7658 7659 if (sibling) { 7660 sibling.innerHTML = ''; 7661 } 7662 return; 7663 } else { 7664 startOffset = dom.nodeIndex(startContainer); 7665 startContainer = startContainer.parentNode; 7666 } 7667 } 7668 7669 if (startOffset == endOffset - 1) { 7670 try { 7671 ctrlRng = body.createControlRange(); 7672 ctrlRng.addElement(startContainer.childNodes[startOffset]); 7673 ctrlRng.select(); 7674 return; 7675 } catch (ex) { 7676 // Ignore 7677 } 7678 } 7679 } 7680 7681 // Set start/end point of selection 7682 setEndPoint(true); 7683 setEndPoint(); 7684 7685 // Select the new range and scroll it into view 7686 ieRng.select(); 7687 }; 7688 7689 // Expose range method 7690 this.getRangeAt = getRange; 7691 }; 7692 7693 // Expose the selection object 7694 tinymce.dom.TridentSelection = Selection; 7695 })(); 7696 7697 7698 (function(tinymce) { 7699 tinymce.dom.Element = function(id, settings) { 7700 var t = this, dom, el; 7701 7702 t.settings = settings = settings || {}; 7703 t.id = id; 7704 t.dom = dom = settings.dom || tinymce.DOM; 7705 7706 // Only IE leaks DOM references, this is a lot faster 7707 if (!tinymce.isIE) 7708 el = dom.get(t.id); 7709 7710 tinymce.each( 7711 ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' + 7712 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' + 7713 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' + 7714 'isHidden,setHTML,get').split(/,/), function(k) { 7715 t[k] = function() { 7716 var a = [id], i; 7717 7718 for (i = 0; i < arguments.length; i++) 7719 a.push(arguments[i]); 7720 7721 a = dom[k].apply(dom, a); 7722 t.update(k); 7723 7724 return a; 7725 }; 7726 } 7727 ); 7728 7729 tinymce.extend(t, { 7730 on : function(n, f, s) { 7731 return tinymce.dom.Event.add(t.id, n, f, s); 7732 }, 7733 7734 getXY : function() { 7735 return { 7736 x : parseInt(t.getStyle('left')), 7737 y : parseInt(t.getStyle('top')) 7738 }; 7739 }, 7740 7741 getSize : function() { 7742 var n = dom.get(t.id); 7743 7744 return { 7745 w : parseInt(t.getStyle('width') || n.clientWidth), 7746 h : parseInt(t.getStyle('height') || n.clientHeight) 7747 }; 7748 }, 7749 7750 moveTo : function(x, y) { 7751 t.setStyles({left : x, top : y}); 7752 }, 7753 7754 moveBy : function(x, y) { 7755 var p = t.getXY(); 7756 7757 t.moveTo(p.x + x, p.y + y); 7758 }, 7759 7760 resizeTo : function(w, h) { 7761 t.setStyles({width : w, height : h}); 7762 }, 7763 7764 resizeBy : function(w, h) { 7765 var s = t.getSize(); 7766 7767 t.resizeTo(s.w + w, s.h + h); 7768 }, 7769 7770 update : function(k) { 7771 var b; 7772 7773 if (tinymce.isIE6 && settings.blocker) { 7774 k = k || ''; 7775 7776 // Ignore getters 7777 if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0) 7778 return; 7779 7780 // Remove blocker on remove 7781 if (k == 'remove') { 7782 dom.remove(t.blocker); 7783 return; 7784 } 7785 7786 if (!t.blocker) { 7787 t.blocker = dom.uniqueId(); 7788 b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'}); 7789 dom.setStyle(b, 'opacity', 0); 7790 } else 7791 b = dom.get(t.blocker); 7792 7793 dom.setStyles(b, { 7794 left : t.getStyle('left', 1), 7795 top : t.getStyle('top', 1), 7796 width : t.getStyle('width', 1), 7797 height : t.getStyle('height', 1), 7798 display : t.getStyle('display', 1), 7799 zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1 7800 }); 7801 } 7802 } 7803 }); 7804 }; 7805 })(tinymce); 7806 7807 (function(tinymce) { 7808 function trimNl(s) { 7809 return s.replace(/[\n\r]+/g, ''); 7810 }; 7811 7812 // Shorten names 7813 var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each, TreeWalker = tinymce.dom.TreeWalker; 7814 7815 tinymce.create('tinymce.dom.Selection', { 7816 Selection : function(dom, win, serializer, editor) { 7817 var t = this; 7818 7819 t.dom = dom; 7820 t.win = win; 7821 t.serializer = serializer; 7822 t.editor = editor; 7823 7824 // Add events 7825 each([ 7826 'onBeforeSetContent', 7827 7828 'onBeforeGetContent', 7829 7830 'onSetContent', 7831 7832 'onGetContent' 7833 ], function(e) { 7834 t[e] = new tinymce.util.Dispatcher(t); 7835 }); 7836 7837 // No W3C Range support 7838 if (!t.win.getSelection) 7839 t.tridentSel = new tinymce.dom.TridentSelection(t); 7840 7841 if (tinymce.isIE && dom.boxModel) 7842 this._fixIESelection(); 7843 7844 // Prevent leaks 7845 tinymce.addUnload(t.destroy, t); 7846 }, 7847 7848 setCursorLocation: function(node, offset) { 7849 var t = this; var r = t.dom.createRng(); 7850 r.setStart(node, offset); 7851 r.setEnd(node, offset); 7852 t.setRng(r); 7853 t.collapse(false); 7854 }, 7855 getContent : function(s) { 7856 var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; 7857 7858 s = s || {}; 7859 wb = wa = ''; 7860 s.get = true; 7861 s.format = s.format || 'html'; 7862 s.forced_root_block = ''; 7863 t.onBeforeGetContent.dispatch(t, s); 7864 7865 if (s.format == 'text') 7866 return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); 7867 7868 if (r.cloneContents) { 7869 n = r.cloneContents(); 7870 7871 if (n) 7872 e.appendChild(n); 7873 } else if (is(r.item) || is(r.htmlText)) { 7874 // IE will produce invalid markup if elements are present that 7875 // it doesn't understand like custom elements or HTML5 elements. 7876 // Adding a BR in front of the contents and then remoiving it seems to fix it though. 7877 e.innerHTML = '<br>' + (r.item ? r.item(0).outerHTML : r.htmlText); 7878 e.removeChild(e.firstChild); 7879 } else 7880 e.innerHTML = r.toString(); 7881 7882 // Keep whitespace before and after 7883 if (/^\s/.test(e.innerHTML)) 7884 wb = ' '; 7885 7886 if (/\s+$/.test(e.innerHTML)) 7887 wa = ' '; 7888 7889 s.getInner = true; 7890 7891 s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; 7892 t.onGetContent.dispatch(t, s); 7893 7894 return s.content; 7895 }, 7896 7897 setContent : function(content, args) { 7898 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; 7899 7900 args = args || {format : 'html'}; 7901 args.set = true; 7902 content = args.content = content; 7903 7904 // Dispatch before set content event 7905 if (!args.no_events) 7906 self.onBeforeSetContent.dispatch(self, args); 7907 7908 content = args.content; 7909 7910 if (rng.insertNode) { 7911 // Make caret marker since insertNode places the caret in the beginning of text after insert 7912 content += '<span id="__caret">_</span>'; 7913 7914 // Delete and insert new node 7915 if (rng.startContainer == doc && rng.endContainer == doc) { 7916 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents 7917 doc.body.innerHTML = content; 7918 } else { 7919 rng.deleteContents(); 7920 7921 if (doc.body.childNodes.length === 0) { 7922 doc.body.innerHTML = content; 7923 } else { 7924 // createContextualFragment doesn't exists in IE 9 DOMRanges 7925 if (rng.createContextualFragment) { 7926 rng.insertNode(rng.createContextualFragment(content)); 7927 } else { 7928 // Fake createContextualFragment call in IE 9 7929 frag = doc.createDocumentFragment(); 7930 temp = doc.createElement('div'); 7931 7932 frag.appendChild(temp); 7933 temp.outerHTML = content; 7934 7935 rng.insertNode(frag); 7936 } 7937 } 7938 } 7939 7940 // Move to caret marker 7941 caretNode = self.dom.get('__caret'); 7942 7943 // Make sure we wrap it compleatly, Opera fails with a simple select call 7944 rng = doc.createRange(); 7945 rng.setStartBefore(caretNode); 7946 rng.setEndBefore(caretNode); 7947 self.setRng(rng); 7948 7949 // Remove the caret position 7950 self.dom.remove('__caret'); 7951 7952 try { 7953 self.setRng(rng); 7954 } catch (ex) { 7955 // Might fail on Opera for some odd reason 7956 } 7957 } else { 7958 if (rng.item) { 7959 // Delete content and get caret text selection 7960 doc.execCommand('Delete', false, null); 7961 rng = self.getRng(); 7962 } 7963 7964 // Explorer removes spaces from the beginning of pasted contents 7965 if (/^\s+/.test(content)) { 7966 rng.pasteHTML('<span id="__mce_tmp">_</span>' + content); 7967 self.dom.remove('__mce_tmp'); 7968 } else 7969 rng.pasteHTML(content); 7970 } 7971 7972 // Dispatch set content event 7973 if (!args.no_events) 7974 self.onSetContent.dispatch(self, args); 7975 }, 7976 7977 getStart : function() { 7978 var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; 7979 7980 if (rng.duplicate || rng.item) { 7981 // Control selection, return first item 7982 if (rng.item) 7983 return rng.item(0); 7984 7985 // Get start element 7986 checkRng = rng.duplicate(); 7987 checkRng.collapse(1); 7988 startElement = checkRng.parentElement(); 7989 if (startElement.ownerDocument !== self.dom.doc) { 7990 startElement = self.dom.getRoot(); 7991 } 7992 7993 // Check if range parent is inside the start element, then return the inner parent element 7994 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element 7995 parentElement = node = rng.parentElement(); 7996 while (node = node.parentNode) { 7997 if (node == startElement) { 7998 startElement = parentElement; 7999 break; 8000 } 8001 } 8002 8003 return startElement; 8004 } else { 8005 startElement = rng.startContainer; 8006 8007 if (startElement.nodeType == 1 && startElement.hasChildNodes()) 8008 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; 8009 8010 if (startElement && startElement.nodeType == 3) 8011 return startElement.parentNode; 8012 8013 return startElement; 8014 } 8015 }, 8016 8017 getEnd : function() { 8018 var self = this, rng = self.getRng(), endElement, endOffset; 8019 8020 if (rng.duplicate || rng.item) { 8021 if (rng.item) 8022 return rng.item(0); 8023 8024 rng = rng.duplicate(); 8025 rng.collapse(0); 8026 endElement = rng.parentElement(); 8027 if (endElement.ownerDocument !== self.dom.doc) { 8028 endElement = self.dom.getRoot(); 8029 } 8030 8031 if (endElement && endElement.nodeName == 'BODY') 8032 return endElement.lastChild || endElement; 8033 8034 return endElement; 8035 } else { 8036 endElement = rng.endContainer; 8037 endOffset = rng.endOffset; 8038 8039 if (endElement.nodeType == 1 && endElement.hasChildNodes()) 8040 endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; 8041 8042 if (endElement && endElement.nodeType == 3) 8043 return endElement.parentNode; 8044 8045 return endElement; 8046 } 8047 }, 8048 8049 getBookmark : function(type, normalized) { 8050 var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; 8051 8052 function findIndex(name, element) { 8053 var index = 0; 8054 8055 each(dom.select(name), function(node, i) { 8056 if (node == element) 8057 index = i; 8058 }); 8059 8060 return index; 8061 }; 8062 8063 function normalizeTableCellSelection(rng) { 8064 function moveEndPoint(start) { 8065 var container, offset, childNodes, prefix = start ? 'start' : 'end'; 8066 8067 container = rng[prefix + 'Container']; 8068 offset = rng[prefix + 'Offset']; 8069 8070 if (container.nodeType == 1 && container.nodeName == "TR") { 8071 childNodes = container.childNodes; 8072 container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; 8073 if (container) { 8074 offset = start ? 0 : container.childNodes.length; 8075 rng['set' + (start ? 'Start' : 'End')](container, offset); 8076 } 8077 } 8078 }; 8079 8080 moveEndPoint(true); 8081 moveEndPoint(); 8082 8083 return rng; 8084 }; 8085 8086 function getLocation() { 8087 var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; 8088 8089 function getPoint(rng, start) { 8090 var container = rng[start ? 'startContainer' : 'endContainer'], 8091 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; 8092 8093 if (container.nodeType == 3) { 8094 if (normalized) { 8095 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) 8096 offset += node.nodeValue.length; 8097 } 8098 8099 point.push(offset); 8100 } else { 8101 childNodes = container.childNodes; 8102 8103 if (offset >= childNodes.length && childNodes.length) { 8104 after = 1; 8105 offset = Math.max(0, childNodes.length - 1); 8106 } 8107 8108 point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); 8109 } 8110 8111 for (; container && container != root; container = container.parentNode) 8112 point.push(t.dom.nodeIndex(container, normalized)); 8113 8114 return point; 8115 }; 8116 8117 bookmark.start = getPoint(rng, true); 8118 8119 if (!t.isCollapsed()) 8120 bookmark.end = getPoint(rng); 8121 8122 return bookmark; 8123 }; 8124 8125 if (type == 2) { 8126 if (t.tridentSel) 8127 return t.tridentSel.getBookmark(type); 8128 8129 return getLocation(); 8130 } 8131 8132 // Handle simple range 8133 if (type) 8134 return {rng : t.getRng()}; 8135 8136 rng = t.getRng(); 8137 id = dom.uniqueId(); 8138 collapsed = tinyMCE.activeEditor.selection.isCollapsed(); 8139 styles = 'overflow:hidden;line-height:0px'; 8140 8141 // Explorer method 8142 if (rng.duplicate || rng.item) { 8143 // Text selection 8144 if (!rng.item) { 8145 rng2 = rng.duplicate(); 8146 8147 try { 8148 // Insert start marker 8149 rng.collapse(); 8150 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>'); 8151 8152 // Insert end marker 8153 if (!collapsed) { 8154 rng2.collapse(false); 8155 8156 // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p> 8157 rng.moveToElementText(rng2.parentElement()); 8158 if (rng.compareEndPoints('StartToEnd', rng2) === 0) 8159 rng2.move('character', -1); 8160 8161 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>'); 8162 } 8163 } catch (ex) { 8164 // IE might throw unspecified error so lets ignore it 8165 return null; 8166 } 8167 } else { 8168 // Control selection 8169 element = rng.item(0); 8170 name = element.nodeName; 8171 8172 return {name : name, index : findIndex(name, element)}; 8173 } 8174 } else { 8175 element = t.getNode(); 8176 name = element.nodeName; 8177 if (name == 'IMG') 8178 return {name : name, index : findIndex(name, element)}; 8179 8180 // W3C method 8181 rng2 = normalizeTableCellSelection(rng.cloneRange()); 8182 8183 // Insert end marker 8184 if (!collapsed) { 8185 rng2.collapse(false); 8186 rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr)); 8187 } 8188 8189 rng = normalizeTableCellSelection(rng); 8190 rng.collapse(true); 8191 rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr)); 8192 } 8193 8194 t.moveToBookmark({id : id, keep : 1}); 8195 8196 return {id : id}; 8197 }, 8198 8199 moveToBookmark : function(bookmark) { 8200 var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; 8201 8202 function setEndPoint(start) { 8203 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; 8204 8205 if (point) { 8206 offset = point[0]; 8207 8208 // Find container node 8209 for (node = root, i = point.length - 1; i >= 1; i--) { 8210 children = node.childNodes; 8211 8212 if (point[i] > children.length - 1) 8213 return; 8214 8215 node = children[point[i]]; 8216 } 8217 8218 // Move text offset to best suitable location 8219 if (node.nodeType === 3) 8220 offset = Math.min(point[0], node.nodeValue.length); 8221 8222 // Move element offset to best suitable location 8223 if (node.nodeType === 1) 8224 offset = Math.min(point[0], node.childNodes.length); 8225 8226 // Set offset within container node 8227 if (start) 8228 rng.setStart(node, offset); 8229 else 8230 rng.setEnd(node, offset); 8231 } 8232 8233 return true; 8234 }; 8235 8236 function restoreEndPoint(suffix) { 8237 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; 8238 8239 if (marker) { 8240 node = marker.parentNode; 8241 8242 if (suffix == 'start') { 8243 if (!keep) { 8244 idx = dom.nodeIndex(marker); 8245 } else { 8246 node = marker.firstChild; 8247 idx = 1; 8248 } 8249 8250 startContainer = endContainer = node; 8251 startOffset = endOffset = idx; 8252 } else { 8253 if (!keep) { 8254 idx = dom.nodeIndex(marker); 8255 } else { 8256 node = marker.firstChild; 8257 idx = 1; 8258 } 8259 8260 endContainer = node; 8261 endOffset = idx; 8262 } 8263 8264 if (!keep) { 8265 prev = marker.previousSibling; 8266 next = marker.nextSibling; 8267 8268 // Remove all marker text nodes 8269 each(tinymce.grep(marker.childNodes), function(node) { 8270 if (node.nodeType == 3) 8271 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); 8272 }); 8273 8274 // Remove marker but keep children if for example contents where inserted into the marker 8275 // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature 8276 while (marker = dom.get(bookmark.id + '_' + suffix)) 8277 dom.remove(marker, 1); 8278 8279 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node 8280 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact 8281 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) { 8282 idx = prev.nodeValue.length; 8283 prev.appendData(next.nodeValue); 8284 dom.remove(next); 8285 8286 if (suffix == 'start') { 8287 startContainer = endContainer = prev; 8288 startOffset = endOffset = idx; 8289 } else { 8290 endContainer = prev; 8291 endOffset = idx; 8292 } 8293 } 8294 } 8295 } 8296 }; 8297 8298 function addBogus(node) { 8299 // Adds a bogus BR element for empty block elements 8300 if (dom.isBlock(node) && !node.innerHTML && !isIE) 8301 node.innerHTML = '<br data-mce-bogus="1" />'; 8302 8303 return node; 8304 }; 8305 8306 if (bookmark) { 8307 if (bookmark.start) { 8308 rng = dom.createRng(); 8309 root = dom.getRoot(); 8310 8311 if (t.tridentSel) 8312 return t.tridentSel.moveToBookmark(bookmark); 8313 8314 if (setEndPoint(true) && setEndPoint()) { 8315 t.setRng(rng); 8316 } 8317 } else if (bookmark.id) { 8318 // Restore start/end points 8319 restoreEndPoint('start'); 8320 restoreEndPoint('end'); 8321 8322 if (startContainer) { 8323 rng = dom.createRng(); 8324 rng.setStart(addBogus(startContainer), startOffset); 8325 rng.setEnd(addBogus(endContainer), endOffset); 8326 t.setRng(rng); 8327 } 8328 } else if (bookmark.name) { 8329 t.select(dom.select(bookmark.name)[bookmark.index]); 8330 } else if (bookmark.rng) 8331 t.setRng(bookmark.rng); 8332 } 8333 }, 8334 8335 select : function(node, content) { 8336 var t = this, dom = t.dom, rng = dom.createRng(), idx; 8337 8338 function setPoint(node, start) { 8339 var walker = new TreeWalker(node, node); 8340 8341 do { 8342 // Text node 8343 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length !== 0) { 8344 if (start) 8345 rng.setStart(node, 0); 8346 else 8347 rng.setEnd(node, node.nodeValue.length); 8348 8349 return; 8350 } 8351 8352 // BR element 8353 if (node.nodeName == 'BR') { 8354 if (start) 8355 rng.setStartBefore(node); 8356 else 8357 rng.setEndBefore(node); 8358 8359 return; 8360 } 8361 } while (node = (start ? walker.next() : walker.prev())); 8362 }; 8363 8364 if (node) { 8365 idx = dom.nodeIndex(node); 8366 rng.setStart(node.parentNode, idx); 8367 rng.setEnd(node.parentNode, idx + 1); 8368 8369 // Find first/last text node or BR element 8370 if (content) { 8371 setPoint(node, 1); 8372 setPoint(node); 8373 } 8374 8375 t.setRng(rng); 8376 } 8377 8378 return node; 8379 }, 8380 8381 isCollapsed : function() { 8382 var t = this, r = t.getRng(), s = t.getSel(); 8383 8384 if (!r || r.item) 8385 return false; 8386 8387 if (r.compareEndPoints) 8388 return r.compareEndPoints('StartToEnd', r) === 0; 8389 8390 return !s || r.collapsed; 8391 }, 8392 8393 collapse : function(to_start) { 8394 var self = this, rng = self.getRng(), node; 8395 8396 // Control range on IE 8397 if (rng.item) { 8398 node = rng.item(0); 8399 rng = self.win.document.body.createTextRange(); 8400 rng.moveToElementText(node); 8401 } 8402 8403 rng.collapse(!!to_start); 8404 self.setRng(rng); 8405 }, 8406 8407 getSel : function() { 8408 var t = this, w = this.win; 8409 8410 return w.getSelection ? w.getSelection() : w.document.selection; 8411 }, 8412 8413 getRng : function(w3c) { 8414 var self = this, selection, rng, elm, doc = self.win.document; 8415 8416 // Found tridentSel object then we need to use that one 8417 if (w3c && self.tridentSel) { 8418 return self.tridentSel.getRangeAt(0); 8419 } 8420 8421 try { 8422 if (selection = self.getSel()) { 8423 rng = selection.rangeCount > 0 ? selection.getRangeAt(0) : (selection.createRange ? selection.createRange() : doc.createRange()); 8424 } 8425 } catch (ex) { 8426 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe 8427 } 8428 8429 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet 8430 if (tinymce.isIE && rng && rng.setStart && doc.selection.createRange().item) { 8431 elm = doc.selection.createRange().item(0); 8432 rng = doc.createRange(); 8433 rng.setStartBefore(elm); 8434 rng.setEndAfter(elm); 8435 } 8436 8437 // No range found then create an empty one 8438 // This can occur when the editor is placed in a hidden container element on Gecko 8439 // Or on IE when there was an exception 8440 if (!rng) { 8441 rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); 8442 } 8443 8444 // If range is at start of document then move it to start of body 8445 if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { 8446 elm = self.dom.getRoot(); 8447 rng.setStart(elm, 0); 8448 rng.setEnd(elm, 0); 8449 } 8450 8451 if (self.selectedRange && self.explicitRange) { 8452 if (rng.compareBoundaryPoints(rng.START_TO_START, self.selectedRange) === 0 && rng.compareBoundaryPoints(rng.END_TO_END, self.selectedRange) === 0) { 8453 // Safari, Opera and Chrome only ever select text which causes the range to change. 8454 // This lets us use the originally set range if the selection hasn't been changed by the user. 8455 rng = self.explicitRange; 8456 } else { 8457 self.selectedRange = null; 8458 self.explicitRange = null; 8459 } 8460 } 8461 8462 return rng; 8463 }, 8464 8465 setRng : function(r, forward) { 8466 var s, t = this; 8467 8468 if (!t.tridentSel) { 8469 s = t.getSel(); 8470 8471 if (s) { 8472 t.explicitRange = r; 8473 8474 try { 8475 s.removeAllRanges(); 8476 } catch (ex) { 8477 // IE9 might throw errors here don't know why 8478 } 8479 8480 s.addRange(r); 8481 8482 // Forward is set to false and we have an extend function 8483 if (forward === false && s.extend) { 8484 s.collapse(r.endContainer, r.endOffset); 8485 s.extend(r.startContainer, r.startOffset); 8486 } 8487 8488 // adding range isn't always successful so we need to check range count otherwise an exception can occur 8489 t.selectedRange = s.rangeCount > 0 ? s.getRangeAt(0) : null; 8490 } 8491 } else { 8492 // Is W3C Range 8493 if (r.cloneRange) { 8494 try { 8495 t.tridentSel.addRange(r); 8496 return; 8497 } catch (ex) { 8498 //IE9 throws an error here if called before selection is placed in the editor 8499 } 8500 } 8501 8502 // Is IE specific range 8503 try { 8504 r.select(); 8505 } catch (ex) { 8506 // Needed for some odd IE bug #1843306 8507 } 8508 } 8509 }, 8510 8511 setNode : function(n) { 8512 var t = this; 8513 8514 t.setContent(t.dom.getOuterHTML(n)); 8515 8516 return n; 8517 }, 8518 8519 getNode : function() { 8520 var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer; 8521 8522 function skipEmptyTextNodes(n, forwards) { 8523 var orig = n; 8524 while (n && n.nodeType === 3 && n.length === 0) { 8525 n = forwards ? n.nextSibling : n.previousSibling; 8526 } 8527 return n || orig; 8528 }; 8529 8530 // Range maybe lost after the editor is made visible again 8531 if (!rng) 8532 return t.dom.getRoot(); 8533 8534 if (rng.setStart) { 8535 elm = rng.commonAncestorContainer; 8536 8537 // Handle selection a image or other control like element such as anchors 8538 if (!rng.collapsed) { 8539 if (rng.startContainer == rng.endContainer) { 8540 if (rng.endOffset - rng.startOffset < 2) { 8541 if (rng.startContainer.hasChildNodes()) 8542 elm = rng.startContainer.childNodes[rng.startOffset]; 8543 } 8544 } 8545 8546 // If the anchor node is a element instead of a text node then return this element 8547 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 8548 // return sel.anchorNode.childNodes[sel.anchorOffset]; 8549 8550 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. 8551 // This happens when you double click an underlined word in FireFox. 8552 if (start.nodeType === 3 && end.nodeType === 3) { 8553 if (start.length === rng.startOffset) { 8554 start = skipEmptyTextNodes(start.nextSibling, true); 8555 } else { 8556 start = start.parentNode; 8557 } 8558 if (rng.endOffset === 0) { 8559 end = skipEmptyTextNodes(end.previousSibling, false); 8560 } else { 8561 end = end.parentNode; 8562 } 8563 8564 if (start && start === end) 8565 return start; 8566 } 8567 } 8568 8569 if (elm && elm.nodeType == 3) 8570 return elm.parentNode; 8571 8572 return elm; 8573 } 8574 8575 return rng.item ? rng.item(0) : rng.parentElement(); 8576 }, 8577 8578 getSelectedBlocks : function(st, en) { 8579 var t = this, dom = t.dom, sb, eb, n, bl = []; 8580 8581 sb = dom.getParent(st || t.getStart(), dom.isBlock); 8582 eb = dom.getParent(en || t.getEnd(), dom.isBlock); 8583 8584 if (sb) 8585 bl.push(sb); 8586 8587 if (sb && eb && sb != eb) { 8588 n = sb; 8589 8590 var walker = new TreeWalker(sb, dom.getRoot()); 8591 while ((n = walker.next()) && n != eb) { 8592 if (dom.isBlock(n)) 8593 bl.push(n); 8594 } 8595 } 8596 8597 if (eb && sb != eb) 8598 bl.push(eb); 8599 8600 return bl; 8601 }, 8602 8603 isForward: function(){ 8604 var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; 8605 8606 // No support for selection direction then always return true 8607 if (!sel || sel.anchorNode == null || sel.focusNode == null) { 8608 return true; 8609 } 8610 8611 anchorRange = dom.createRng(); 8612 anchorRange.setStart(sel.anchorNode, sel.anchorOffset); 8613 anchorRange.collapse(true); 8614 8615 focusRange = dom.createRng(); 8616 focusRange.setStart(sel.focusNode, sel.focusOffset); 8617 focusRange.collapse(true); 8618 8619 return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; 8620 }, 8621 8622 normalize : function() { 8623 var self = this, rng, normalized, collapsed, node, sibling; 8624 8625 function normalizeEndPoint(start) { 8626 var container, offset, walker, dom = self.dom, body = dom.getRoot(), node, nonEmptyElementsMap, nodeName; 8627 8628 function hasBrBeforeAfter(node, left) { 8629 var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); 8630 8631 while (node = walker[left ? 'prev' : 'next']()) { 8632 if (node.nodeName === "BR") { 8633 return true; 8634 } 8635 } 8636 }; 8637 8638 // Walks the dom left/right to find a suitable text node to move the endpoint into 8639 // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG 8640 function findTextNodeRelative(left, startNode) { 8641 var walker, lastInlineElement; 8642 8643 startNode = startNode || container; 8644 walker = new TreeWalker(startNode, dom.getParent(startNode.parentNode, dom.isBlock) || body); 8645 8646 // Walk left until we hit a text node we can move to or a block/br/img 8647 while (node = walker[left ? 'prev' : 'next']()) { 8648 // Found text node that has a length 8649 if (node.nodeType === 3 && node.nodeValue.length > 0) { 8650 container = node; 8651 offset = left ? node.nodeValue.length : 0; 8652 normalized = true; 8653 return; 8654 } 8655 8656 // Break if we find a block or a BR/IMG/INPUT etc 8657 if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 8658 return; 8659 } 8660 8661 lastInlineElement = node; 8662 } 8663 8664 // Only fetch the last inline element when in caret mode for now 8665 if (collapsed && lastInlineElement) { 8666 container = lastInlineElement; 8667 normalized = true; 8668 offset = 0; 8669 } 8670 }; 8671 8672 container = rng[(start ? 'start' : 'end') + 'Container']; 8673 offset = rng[(start ? 'start' : 'end') + 'Offset']; 8674 nonEmptyElementsMap = dom.schema.getNonEmptyElements(); 8675 8676 // If the container is a document move it to the body element 8677 if (container.nodeType === 9) { 8678 container = dom.getRoot(); 8679 offset = 0; 8680 } 8681 8682 // If the container is body try move it into the closest text node or position 8683 if (container === body) { 8684 // If start is before/after a image, table etc 8685 if (start) { 8686 node = container.childNodes[offset > 0 ? offset - 1 : 0]; 8687 if (node) { 8688 nodeName = node.nodeName.toLowerCase(); 8689 if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { 8690 return; 8691 } 8692 } 8693 } 8694 8695 // Resolve the index 8696 if (container.hasChildNodes()) { 8697 container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)]; 8698 offset = 0; 8699 8700 // Don't walk into elements that doesn't have any child nodes like a IMG 8701 if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { 8702 // Walk the DOM to find a text node to place the caret at or a BR 8703 node = container; 8704 walker = new TreeWalker(container, body); 8705 8706 do { 8707 // Found a text node use that position 8708 if (node.nodeType === 3 && node.nodeValue.length > 0) { 8709 offset = start ? 0 : node.nodeValue.length; 8710 container = node; 8711 normalized = true; 8712 break; 8713 } 8714 8715 // Found a BR/IMG element that we can place the caret before 8716 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 8717 offset = dom.nodeIndex(node); 8718 container = node.parentNode; 8719 8720 // Put caret after image when moving the end point 8721 if (node.nodeName == "IMG" && !start) { 8722 offset++; 8723 } 8724 8725 normalized = true; 8726 break; 8727 } 8728 } while (node = (start ? walker.next() : walker.prev())); 8729 } 8730 } 8731 } 8732 8733 // Lean the caret to the left if possible 8734 if (collapsed) { 8735 // So this: <b>x</b><i>|x</i> 8736 // Becomes: <b>x|</b><i>x</i> 8737 // Seems that only gecko has issues with this 8738 if (container.nodeType === 3 && offset === 0) { 8739 findTextNodeRelative(true); 8740 } 8741 8742 // Lean left into empty inline elements when the caret is before a BR 8743 // So this: <i><b></b><i>|<br></i> 8744 // Becomes: <i><b>|</b><i><br></i> 8745 // Seems that only gecko has issues with this 8746 if (container.nodeType === 1) { 8747 node = container.childNodes[offset]; 8748 if(node && node.nodeName === 'BR' && !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { 8749 findTextNodeRelative(true, container.childNodes[offset]); 8750 } 8751 } 8752 } 8753 8754 // Lean the start of the selection right if possible 8755 // So this: x[<b>x]</b> 8756 // Becomes: x<b>[x]</b> 8757 if (start && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { 8758 findTextNodeRelative(false); 8759 } 8760 8761 // Set endpoint if it was normalized 8762 if (normalized) 8763 rng['set' + (start ? 'Start' : 'End')](container, offset); 8764 }; 8765 8766 // Normalize only on non IE browsers for now 8767 if (tinymce.isIE) 8768 return; 8769 8770 rng = self.getRng(); 8771 collapsed = rng.collapsed; 8772 8773 // Normalize the end points 8774 normalizeEndPoint(true); 8775 8776 if (!collapsed) 8777 normalizeEndPoint(); 8778 8779 // Set the selection if it was normalized 8780 if (normalized) { 8781 // If it was collapsed then make sure it still is 8782 if (collapsed) { 8783 rng.collapse(true); 8784 } 8785 8786 //console.log(self.dom.dumpRng(rng)); 8787 self.setRng(rng, self.isForward()); 8788 } 8789 }, 8790 8791 selectorChanged: function(selector, callback) { 8792 var self = this, currentSelectors; 8793 8794 if (!self.selectorChangedData) { 8795 self.selectorChangedData = {}; 8796 currentSelectors = {}; 8797 8798 self.editor.onNodeChange.addToTop(function(ed, cm, node) { 8799 var dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; 8800 8801 // Check for new matching selectors 8802 each(self.selectorChangedData, function(callbacks, selector) { 8803 each(parents, function(node) { 8804 if (dom.is(node, selector)) { 8805 if (!currentSelectors[selector]) { 8806 // Execute callbacks 8807 each(callbacks, function(callback) { 8808 callback(true, {node: node, selector: selector, parents: parents}); 8809 }); 8810 8811 currentSelectors[selector] = callbacks; 8812 } 8813 8814 matchedSelectors[selector] = callbacks; 8815 return false; 8816 } 8817 }); 8818 }); 8819 8820 // Check if current selectors still match 8821 each(currentSelectors, function(callbacks, selector) { 8822 if (!matchedSelectors[selector]) { 8823 delete currentSelectors[selector]; 8824 8825 each(callbacks, function(callback) { 8826 callback(false, {node: node, selector: selector, parents: parents}); 8827 }); 8828 } 8829 }); 8830 }); 8831 } 8832 8833 // Add selector listeners 8834 if (!self.selectorChangedData[selector]) { 8835 self.selectorChangedData[selector] = []; 8836 } 8837 8838 self.selectorChangedData[selector].push(callback); 8839 8840 return self; 8841 }, 8842 8843 destroy : function(manual) { 8844 var self = this; 8845 8846 self.win = null; 8847 8848 // Manual destroy then remove unload handler 8849 if (!manual) 8850 tinymce.removeUnload(self.destroy); 8851 }, 8852 8853 // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode 8854 _fixIESelection : function() { 8855 var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm; 8856 8857 // Return range from point or null if it failed 8858 function rngFromPoint(x, y) { 8859 var rng = body.createTextRange(); 8860 8861 try { 8862 rng.moveToPoint(x, y); 8863 } catch (ex) { 8864 // IE sometimes throws and exception, so lets just ignore it 8865 rng = null; 8866 } 8867 8868 return rng; 8869 }; 8870 8871 // Fires while the selection is changing 8872 function selectionChange(e) { 8873 var pointRng; 8874 8875 // Check if the button is down or not 8876 if (e.button) { 8877 // Create range from mouse position 8878 pointRng = rngFromPoint(e.x, e.y); 8879 8880 if (pointRng) { 8881 // Check if pointRange is before/after selection then change the endPoint 8882 if (pointRng.compareEndPoints('StartToStart', startRng) > 0) 8883 pointRng.setEndPoint('StartToStart', startRng); 8884 else 8885 pointRng.setEndPoint('EndToEnd', startRng); 8886 8887 pointRng.select(); 8888 } 8889 } else 8890 endSelection(); 8891 } 8892 8893 // Removes listeners 8894 function endSelection() { 8895 var rng = doc.selection.createRange(); 8896 8897 // If the range is collapsed then use the last start range 8898 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) 8899 startRng.select(); 8900 8901 dom.unbind(doc, 'mouseup', endSelection); 8902 dom.unbind(doc, 'mousemove', selectionChange); 8903 startRng = started = 0; 8904 }; 8905 8906 // Make HTML element unselectable since we are going to handle selection by hand 8907 doc.documentElement.unselectable = true; 8908 8909 // Detect when user selects outside BODY 8910 dom.bind(doc, ['mousedown', 'contextmenu'], function(e) { 8911 if (e.target.nodeName === 'HTML') { 8912 if (started) 8913 endSelection(); 8914 8915 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML 8916 htmlElm = doc.documentElement; 8917 if (htmlElm.scrollHeight > htmlElm.clientHeight) 8918 return; 8919 8920 started = 1; 8921 // Setup start position 8922 startRng = rngFromPoint(e.x, e.y); 8923 if (startRng) { 8924 // Listen for selection change events 8925 dom.bind(doc, 'mouseup', endSelection); 8926 dom.bind(doc, 'mousemove', selectionChange); 8927 8928 dom.win.focus(); 8929 startRng.select(); 8930 } 8931 } 8932 }); 8933 } 8934 }); 8935 })(tinymce); 8936 8937 (function(tinymce) { 8938 tinymce.dom.Serializer = function(settings, dom, schema) { 8939 var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser; 8940 8941 // Support the old apply_source_formatting option 8942 if (!settings.apply_source_formatting) 8943 settings.indent = false; 8944 8945 // Default DOM and Schema if they are undefined 8946 dom = dom || tinymce.DOM; 8947 schema = schema || new tinymce.html.Schema(settings); 8948 settings.entity_encoding = settings.entity_encoding || 'named'; 8949 settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; 8950 8951 onPreProcess = new tinymce.util.Dispatcher(self); 8952 8953 onPostProcess = new tinymce.util.Dispatcher(self); 8954 8955 htmlParser = new tinymce.html.DomParser(settings, schema); 8956 8957 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed 8958 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { 8959 var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; 8960 8961 while (i--) { 8962 node = nodes[i]; 8963 8964 value = node.attributes.map[internalName]; 8965 if (value !== undef) { 8966 // Set external name to internal value and remove internal 8967 node.attr(name, value.length > 0 ? value : null); 8968 node.attr(internalName, null); 8969 } else { 8970 // No internal attribute found then convert the value we have in the DOM 8971 value = node.attributes.map[name]; 8972 8973 if (name === "style") 8974 value = dom.serializeStyle(dom.parseStyle(value), node.name); 8975 else if (urlConverter) 8976 value = urlConverter.call(urlConverterScope, value, name, node.name); 8977 8978 node.attr(name, value.length > 0 ? value : null); 8979 } 8980 } 8981 }); 8982 8983 // Remove internal classes mceItem<..> or mceSelected 8984 htmlParser.addAttributeFilter('class', function(nodes, name) { 8985 var i = nodes.length, node, value; 8986 8987 while (i--) { 8988 node = nodes[i]; 8989 value = node.attr('class').replace(/(?:^|\s)mce(Item\w+|Selected)(?!\S)/g, ''); 8990 node.attr('class', value.length > 0 ? value : null); 8991 } 8992 }); 8993 8994 // Remove bookmark elements 8995 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { 8996 var i = nodes.length, node; 8997 8998 while (i--) { 8999 node = nodes[i]; 9000 9001 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) 9002 node.remove(); 9003 } 9004 }); 9005 9006 // Remove expando attributes 9007 htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name, args) { 9008 var i = nodes.length; 9009 9010 while (i--) { 9011 nodes[i].attr(name, null); 9012 } 9013 }); 9014 9015 // Force script into CDATA sections and remove the mce- prefix also add comments around styles 9016 htmlParser.addNodeFilter('script,style', function(nodes, name) { 9017 var i = nodes.length, node, value; 9018 9019 function trim(value) { 9020 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') 9021 .replace(/^[\r\n]*|[\r\n]*$/g, '') 9022 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '') 9023 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); 9024 }; 9025 9026 while (i--) { 9027 node = nodes[i]; 9028 value = node.firstChild ? node.firstChild.value : ''; 9029 9030 if (name === "script") { 9031 // Remove mce- prefix from script elements 9032 node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, '')); 9033 9034 if (value.length > 0) 9035 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; 9036 } else { 9037 if (value.length > 0) 9038 node.firstChild.value = '<!--\n' + trim(value) + '\n-->'; 9039 } 9040 } 9041 }); 9042 9043 // Convert comments to cdata and handle protected comments 9044 htmlParser.addNodeFilter('#comment', function(nodes, name) { 9045 var i = nodes.length, node; 9046 9047 while (i--) { 9048 node = nodes[i]; 9049 9050 if (node.value.indexOf('[CDATA[') === 0) { 9051 node.name = '#cdata'; 9052 node.type = 4; 9053 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); 9054 } else if (node.value.indexOf('mce:protected ') === 0) { 9055 node.name = "#text"; 9056 node.type = 3; 9057 node.raw = true; 9058 node.value = unescape(node.value).substr(14); 9059 } 9060 } 9061 }); 9062 9063 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { 9064 var i = nodes.length, node; 9065 9066 while (i--) { 9067 node = nodes[i]; 9068 if (node.type === 7) 9069 node.remove(); 9070 else if (node.type === 1) { 9071 if (name === "input" && !("type" in node.attributes.map)) 9072 node.attr('type', 'text'); 9073 } 9074 } 9075 }); 9076 9077 // Fix list elements, TODO: Replace this later 9078 if (settings.fix_list_elements) { 9079 htmlParser.addNodeFilter('ul,ol', function(nodes, name) { 9080 var i = nodes.length, node, parentNode; 9081 9082 while (i--) { 9083 node = nodes[i]; 9084 parentNode = node.parent; 9085 9086 if (parentNode.name === 'ul' || parentNode.name === 'ol') { 9087 if (node.prev && node.prev.name === 'li') { 9088 node.prev.append(node); 9089 } 9090 } 9091 } 9092 }); 9093 } 9094 9095 // Remove internal data attributes 9096 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) { 9097 var i = nodes.length; 9098 9099 while (i--) { 9100 nodes[i].attr(name, null); 9101 } 9102 }); 9103 9104 // Return public methods 9105 return { 9106 schema : schema, 9107 9108 addNodeFilter : htmlParser.addNodeFilter, 9109 9110 addAttributeFilter : htmlParser.addAttributeFilter, 9111 9112 onPreProcess : onPreProcess, 9113 9114 onPostProcess : onPostProcess, 9115 9116 serialize : function(node, args) { 9117 var impl, doc, oldDoc, htmlSerializer, content; 9118 9119 // Explorer won't clone contents of script and style and the 9120 // selected index of select elements are cleared on a clone operation. 9121 if (isIE && dom.select('script,style,select,map').length > 0) { 9122 content = node.innerHTML; 9123 node = node.cloneNode(false); 9124 dom.setHTML(node, content); 9125 } else 9126 node = node.cloneNode(true); 9127 9128 // Nodes needs to be attached to something in WebKit/Opera 9129 // Older builds of Opera crashes if you attach the node to an document created dynamically 9130 // and since we can't feature detect a crash we need to sniff the acutal build number 9131 // This fix will make DOM ranges and make Sizzle happy! 9132 impl = node.ownerDocument.implementation; 9133 if (impl.createHTMLDocument) { 9134 // Create an empty HTML document 9135 doc = impl.createHTMLDocument(""); 9136 9137 // Add the element or it's children if it's a body element to the new document 9138 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { 9139 doc.body.appendChild(doc.importNode(node, true)); 9140 }); 9141 9142 // Grab first child or body element for serialization 9143 if (node.nodeName != 'BODY') 9144 node = doc.body.firstChild; 9145 else 9146 node = doc.body; 9147 9148 // set the new document in DOMUtils so createElement etc works 9149 oldDoc = dom.doc; 9150 dom.doc = doc; 9151 } 9152 9153 args = args || {}; 9154 args.format = args.format || 'html'; 9155 9156 // Pre process 9157 if (!args.no_events) { 9158 args.node = node; 9159 onPreProcess.dispatch(self, args); 9160 } 9161 9162 // Setup serializer 9163 htmlSerializer = new tinymce.html.Serializer(settings, schema); 9164 9165 // Parse and serialize HTML 9166 args.content = htmlSerializer.serialize( 9167 htmlParser.parse(tinymce.trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) 9168 ); 9169 9170 // Replace all BOM characters for now until we can find a better solution 9171 if (!args.cleanup) 9172 args.content = args.content.replace(/\uFEFF|\u200B/g, ''); 9173 9174 // Post process 9175 if (!args.no_events) 9176 onPostProcess.dispatch(self, args); 9177 9178 // Restore the old document if it was changed 9179 if (oldDoc) 9180 dom.doc = oldDoc; 9181 9182 args.node = null; 9183 9184 return args.content; 9185 }, 9186 9187 addRules : function(rules) { 9188 schema.addValidElements(rules); 9189 }, 9190 9191 setRules : function(rules) { 9192 schema.setValidElements(rules); 9193 } 9194 }; 9195 }; 9196 })(tinymce); 9197 (function(tinymce) { 9198 tinymce.dom.ScriptLoader = function(settings) { 9199 var QUEUED = 0, 9200 LOADING = 1, 9201 LOADED = 2, 9202 states = {}, 9203 queue = [], 9204 scriptLoadedCallbacks = {}, 9205 queueLoadedCallbacks = [], 9206 loading = 0, 9207 undef; 9208 9209 function loadScript(url, callback) { 9210 var t = this, dom = tinymce.DOM, elm, uri, loc, id; 9211 9212 // Execute callback when script is loaded 9213 function done() { 9214 dom.remove(id); 9215 9216 if (elm) 9217 elm.onreadystatechange = elm.onload = elm = null; 9218 9219 callback(); 9220 }; 9221 9222 function error() { 9223 // Report the error so it's easier for people to spot loading errors 9224 if (typeof(console) !== "undefined" && console.log) 9225 console.log("Failed to load: " + url); 9226 9227 // We can't mark it as done if there is a load error since 9228 // A) We don't want to produce 404 errors on the server and 9229 // B) the onerror event won't fire on all browsers. 9230 // done(); 9231 }; 9232 9233 id = dom.uniqueId(); 9234 9235 if (tinymce.isIE6) { 9236 uri = new tinymce.util.URI(url); 9237 loc = location; 9238 9239 // If script is from same domain and we 9240 // use IE 6 then use XHR since it's more reliable 9241 if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') { 9242 tinymce.util.XHR.send({ 9243 url : tinymce._addVer(uri.getURI()), 9244 success : function(content) { 9245 // Create new temp script element 9246 var script = dom.create('script', { 9247 type : 'text/javascript' 9248 }); 9249 9250 // Evaluate script in global scope 9251 script.text = content; 9252 document.getElementsByTagName('head')[0].appendChild(script); 9253 dom.remove(script); 9254 9255 done(); 9256 }, 9257 9258 error : error 9259 }); 9260 9261 return; 9262 } 9263 } 9264 9265 // Create new script element 9266 elm = document.createElement('script'); 9267 elm.id = id; 9268 elm.type = 'text/javascript'; 9269 elm.src = tinymce._addVer(url); 9270 9271 // Add onload listener for non IE browsers since IE9 9272 // fires onload event before the script is parsed and executed 9273 if (!tinymce.isIE) 9274 elm.onload = done; 9275 9276 // Add onerror event will get fired on some browsers but not all of them 9277 elm.onerror = error; 9278 9279 // Opera 9.60 doesn't seem to fire the onreadystate event at correctly 9280 if (!tinymce.isOpera) { 9281 elm.onreadystatechange = function() { 9282 var state = elm.readyState; 9283 9284 // Loaded state is passed on IE 6 however there 9285 // are known issues with this method but we can't use 9286 // XHR in a cross domain loading 9287 if (state == 'complete' || state == 'loaded') 9288 done(); 9289 }; 9290 } 9291 9292 // Most browsers support this feature so we report errors 9293 // for those at least to help users track their missing plugins etc 9294 // todo: Removed since it produced error if the document is unloaded by navigating away, re-add it as an option 9295 /*elm.onerror = function() { 9296 alert('Failed to load: ' + url); 9297 };*/ 9298 9299 // Add script to document 9300 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); 9301 }; 9302 9303 this.isDone = function(url) { 9304 return states[url] == LOADED; 9305 }; 9306 9307 this.markDone = function(url) { 9308 states[url] = LOADED; 9309 }; 9310 9311 this.add = this.load = function(url, callback, scope) { 9312 var item, state = states[url]; 9313 9314 // Add url to load queue 9315 if (state == undef) { 9316 queue.push(url); 9317 states[url] = QUEUED; 9318 } 9319 9320 if (callback) { 9321 // Store away callback for later execution 9322 if (!scriptLoadedCallbacks[url]) 9323 scriptLoadedCallbacks[url] = []; 9324 9325 scriptLoadedCallbacks[url].push({ 9326 func : callback, 9327 scope : scope || this 9328 }); 9329 } 9330 }; 9331 9332 this.loadQueue = function(callback, scope) { 9333 this.loadScripts(queue, callback, scope); 9334 }; 9335 9336 this.loadScripts = function(scripts, callback, scope) { 9337 var loadScripts; 9338 9339 function execScriptLoadedCallbacks(url) { 9340 // Execute URL callback functions 9341 tinymce.each(scriptLoadedCallbacks[url], function(callback) { 9342 callback.func.call(callback.scope); 9343 }); 9344 9345 scriptLoadedCallbacks[url] = undef; 9346 }; 9347 9348 queueLoadedCallbacks.push({ 9349 func : callback, 9350 scope : scope || this 9351 }); 9352 9353 loadScripts = function() { 9354 var loadingScripts = tinymce.grep(scripts); 9355 9356 // Current scripts has been handled 9357 scripts.length = 0; 9358 9359 // Load scripts that needs to be loaded 9360 tinymce.each(loadingScripts, function(url) { 9361 // Script is already loaded then execute script callbacks directly 9362 if (states[url] == LOADED) { 9363 execScriptLoadedCallbacks(url); 9364 return; 9365 } 9366 9367 // Is script not loading then start loading it 9368 if (states[url] != LOADING) { 9369 states[url] = LOADING; 9370 loading++; 9371 9372 loadScript(url, function() { 9373 states[url] = LOADED; 9374 loading--; 9375 9376 execScriptLoadedCallbacks(url); 9377 9378 // Load more scripts if they where added by the recently loaded script 9379 loadScripts(); 9380 }); 9381 } 9382 }); 9383 9384 // No scripts are currently loading then execute all pending queue loaded callbacks 9385 if (!loading) { 9386 tinymce.each(queueLoadedCallbacks, function(callback) { 9387 callback.func.call(callback.scope); 9388 }); 9389 9390 queueLoadedCallbacks.length = 0; 9391 } 9392 }; 9393 9394 loadScripts(); 9395 }; 9396 }; 9397 9398 // Global script loader 9399 tinymce.ScriptLoader = new tinymce.dom.ScriptLoader(); 9400 })(tinymce); 9401 9402 (function(tinymce) { 9403 tinymce.dom.RangeUtils = function(dom) { 9404 var INVISIBLE_CHAR = '\uFEFF'; 9405 9406 this.walk = function(rng, callback) { 9407 var startContainer = rng.startContainer, 9408 startOffset = rng.startOffset, 9409 endContainer = rng.endContainer, 9410 endOffset = rng.endOffset, 9411 ancestor, startPoint, 9412 endPoint, node, parent, siblings, nodes; 9413 9414 // Handle table cell selection the table plugin enables 9415 // you to fake select table cells and perform formatting actions on them 9416 nodes = dom.select('td.mceSelected,th.mceSelected'); 9417 if (nodes.length > 0) { 9418 tinymce.each(nodes, function(node) { 9419 callback([node]); 9420 }); 9421 9422 return; 9423 } 9424 9425 function exclude(nodes) { 9426 var node; 9427 9428 // First node is excluded 9429 node = nodes[0]; 9430 if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { 9431 nodes.splice(0, 1); 9432 } 9433 9434 // Last node is excluded 9435 node = nodes[nodes.length - 1]; 9436 if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { 9437 nodes.splice(nodes.length - 1, 1); 9438 } 9439 9440 return nodes; 9441 }; 9442 9443 function collectSiblings(node, name, end_node) { 9444 var siblings = []; 9445 9446 for (; node && node != end_node; node = node[name]) 9447 siblings.push(node); 9448 9449 return siblings; 9450 }; 9451 9452 function findEndPoint(node, root) { 9453 do { 9454 if (node.parentNode == root) 9455 return node; 9456 9457 node = node.parentNode; 9458 } while(node); 9459 }; 9460 9461 function walkBoundary(start_node, end_node, next) { 9462 var siblingName = next ? 'nextSibling' : 'previousSibling'; 9463 9464 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { 9465 parent = node.parentNode; 9466 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); 9467 9468 if (siblings.length) { 9469 if (!next) 9470 siblings.reverse(); 9471 9472 callback(exclude(siblings)); 9473 } 9474 } 9475 }; 9476 9477 // If index based start position then resolve it 9478 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) 9479 startContainer = startContainer.childNodes[startOffset]; 9480 9481 // If index based end position then resolve it 9482 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) 9483 endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; 9484 9485 // Same container 9486 if (startContainer == endContainer) 9487 return callback(exclude([startContainer])); 9488 9489 // Find common ancestor and end points 9490 ancestor = dom.findCommonAncestor(startContainer, endContainer); 9491 9492 // Process left side 9493 for (node = startContainer; node; node = node.parentNode) { 9494 if (node === endContainer) 9495 return walkBoundary(startContainer, ancestor, true); 9496 9497 if (node === ancestor) 9498 break; 9499 } 9500 9501 // Process right side 9502 for (node = endContainer; node; node = node.parentNode) { 9503 if (node === startContainer) 9504 return walkBoundary(endContainer, ancestor); 9505 9506 if (node === ancestor) 9507 break; 9508 } 9509 9510 // Find start/end point 9511 startPoint = findEndPoint(startContainer, ancestor) || startContainer; 9512 endPoint = findEndPoint(endContainer, ancestor) || endContainer; 9513 9514 // Walk left leaf 9515 walkBoundary(startContainer, startPoint, true); 9516 9517 // Walk the middle from start to end point 9518 siblings = collectSiblings( 9519 startPoint == startContainer ? startPoint : startPoint.nextSibling, 9520 'nextSibling', 9521 endPoint == endContainer ? endPoint.nextSibling : endPoint 9522 ); 9523 9524 if (siblings.length) 9525 callback(exclude(siblings)); 9526 9527 // Walk right leaf 9528 walkBoundary(endContainer, endPoint); 9529 }; 9530 9531 this.split = function(rng) { 9532 var startContainer = rng.startContainer, 9533 startOffset = rng.startOffset, 9534 endContainer = rng.endContainer, 9535 endOffset = rng.endOffset; 9536 9537 function splitText(node, offset) { 9538 return node.splitText(offset); 9539 }; 9540 9541 // Handle single text node 9542 if (startContainer == endContainer && startContainer.nodeType == 3) { 9543 if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { 9544 endContainer = splitText(startContainer, startOffset); 9545 startContainer = endContainer.previousSibling; 9546 9547 if (endOffset > startOffset) { 9548 endOffset = endOffset - startOffset; 9549 startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; 9550 endOffset = endContainer.nodeValue.length; 9551 startOffset = 0; 9552 } else { 9553 endOffset = 0; 9554 } 9555 } 9556 } else { 9557 // Split startContainer text node if needed 9558 if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { 9559 startContainer = splitText(startContainer, startOffset); 9560 startOffset = 0; 9561 } 9562 9563 // Split endContainer text node if needed 9564 if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { 9565 endContainer = splitText(endContainer, endOffset).previousSibling; 9566 endOffset = endContainer.nodeValue.length; 9567 } 9568 } 9569 9570 return { 9571 startContainer : startContainer, 9572 startOffset : startOffset, 9573 endContainer : endContainer, 9574 endOffset : endOffset 9575 }; 9576 }; 9577 9578 }; 9579 9580 tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) { 9581 if (rng1 && rng2) { 9582 // Compare native IE ranges 9583 if (rng1.item || rng1.duplicate) { 9584 // Both are control ranges and the selected element matches 9585 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) 9586 return true; 9587 9588 // Both are text ranges and the range matches 9589 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) 9590 return true; 9591 } else { 9592 // Compare w3c ranges 9593 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; 9594 } 9595 } 9596 9597 return false; 9598 }; 9599 })(tinymce); 9600 9601 (function(tinymce) { 9602 var Event = tinymce.dom.Event, each = tinymce.each; 9603 9604 tinymce.create('tinymce.ui.KeyboardNavigation', { 9605 KeyboardNavigation: function(settings, dom) { 9606 var t = this, root = settings.root, items = settings.items, 9607 enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown, 9608 excludeFromTabOrder = settings.excludeFromTabOrder, 9609 itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId; 9610 9611 dom = dom || tinymce.DOM; 9612 9613 itemFocussed = function(evt) { 9614 focussedId = evt.target.id; 9615 }; 9616 9617 itemBlurred = function(evt) { 9618 dom.setAttrib(evt.target.id, 'tabindex', '-1'); 9619 }; 9620 9621 rootFocussed = function(evt) { 9622 var item = dom.get(focussedId); 9623 dom.setAttrib(item, 'tabindex', '0'); 9624 item.focus(); 9625 }; 9626 9627 t.focus = function() { 9628 dom.get(focussedId).focus(); 9629 }; 9630 9631 t.destroy = function() { 9632 each(items, function(item) { 9633 var elm = dom.get(item.id); 9634 9635 dom.unbind(elm, 'focus', itemFocussed); 9636 dom.unbind(elm, 'blur', itemBlurred); 9637 }); 9638 9639 var rootElm = dom.get(root); 9640 dom.unbind(rootElm, 'focus', rootFocussed); 9641 dom.unbind(rootElm, 'keydown', rootKeydown); 9642 9643 items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null; 9644 t.destroy = function() {}; 9645 }; 9646 9647 t.moveFocus = function(dir, evt) { 9648 var idx = -1, controls = t.controls, newFocus; 9649 9650 if (!focussedId) 9651 return; 9652 9653 each(items, function(item, index) { 9654 if (item.id === focussedId) { 9655 idx = index; 9656 return false; 9657 } 9658 }); 9659 9660 idx += dir; 9661 if (idx < 0) { 9662 idx = items.length - 1; 9663 } else if (idx >= items.length) { 9664 idx = 0; 9665 } 9666 9667 newFocus = items[idx]; 9668 dom.setAttrib(focussedId, 'tabindex', '-1'); 9669 dom.setAttrib(newFocus.id, 'tabindex', '0'); 9670 dom.get(newFocus.id).focus(); 9671 9672 if (settings.actOnFocus) { 9673 settings.onAction(newFocus.id); 9674 } 9675 9676 if (evt) 9677 Event.cancel(evt); 9678 }; 9679 9680 rootKeydown = function(evt) { 9681 var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32; 9682 9683 switch (evt.keyCode) { 9684 case DOM_VK_LEFT: 9685 if (enableLeftRight) t.moveFocus(-1); 9686 break; 9687 9688 case DOM_VK_RIGHT: 9689 if (enableLeftRight) t.moveFocus(1); 9690 break; 9691 9692 case DOM_VK_UP: 9693 if (enableUpDown) t.moveFocus(-1); 9694 break; 9695 9696 case DOM_VK_DOWN: 9697 if (enableUpDown) t.moveFocus(1); 9698 break; 9699 9700 case DOM_VK_ESCAPE: 9701 if (settings.onCancel) { 9702 settings.onCancel(); 9703 Event.cancel(evt); 9704 } 9705 break; 9706 9707 case DOM_VK_ENTER: 9708 case DOM_VK_RETURN: 9709 case DOM_VK_SPACE: 9710 if (settings.onAction) { 9711 settings.onAction(focussedId); 9712 Event.cancel(evt); 9713 } 9714 break; 9715 } 9716 }; 9717 9718 // Set up state and listeners for each item. 9719 each(items, function(item, idx) { 9720 var tabindex, elm; 9721 9722 if (!item.id) { 9723 item.id = dom.uniqueId('_mce_item_'); 9724 } 9725 9726 elm = dom.get(item.id); 9727 9728 if (excludeFromTabOrder) { 9729 dom.bind(elm, 'blur', itemBlurred); 9730 tabindex = '-1'; 9731 } else { 9732 tabindex = (idx === 0 ? '0' : '-1'); 9733 } 9734 9735 elm.setAttribute('tabindex', tabindex); 9736 dom.bind(elm, 'focus', itemFocussed); 9737 }); 9738 9739 // Setup initial state for root element. 9740 if (items[0]){ 9741 focussedId = items[0].id; 9742 } 9743 9744 dom.setAttrib(root, 'tabindex', '-1'); 9745 9746 // Setup listeners for root element. 9747 var rootElm = dom.get(root); 9748 dom.bind(rootElm, 'focus', rootFocussed); 9749 dom.bind(rootElm, 'keydown', rootKeydown); 9750 } 9751 }); 9752 })(tinymce); 9753 9754 (function(tinymce) { 9755 // Shorten class names 9756 var DOM = tinymce.DOM, is = tinymce.is; 9757 9758 tinymce.create('tinymce.ui.Control', { 9759 Control : function(id, s, editor) { 9760 this.id = id; 9761 this.settings = s = s || {}; 9762 this.rendered = false; 9763 this.onRender = new tinymce.util.Dispatcher(this); 9764 this.classPrefix = ''; 9765 this.scope = s.scope || this; 9766 this.disabled = 0; 9767 this.active = 0; 9768 this.editor = editor; 9769 }, 9770 9771 setAriaProperty : function(property, value) { 9772 var element = DOM.get(this.id + '_aria') || DOM.get(this.id); 9773 if (element) { 9774 DOM.setAttrib(element, 'aria-' + property, !!value); 9775 } 9776 }, 9777 9778 focus : function() { 9779 DOM.get(this.id).focus(); 9780 }, 9781 9782 setDisabled : function(s) { 9783 if (s != this.disabled) { 9784 this.setAriaProperty('disabled', s); 9785 9786 this.setState('Disabled', s); 9787 this.setState('Enabled', !s); 9788 this.disabled = s; 9789 } 9790 }, 9791 9792 isDisabled : function() { 9793 return this.disabled; 9794 }, 9795 9796 setActive : function(s) { 9797 if (s != this.active) { 9798 this.setState('Active', s); 9799 this.active = s; 9800 this.setAriaProperty('pressed', s); 9801 } 9802 }, 9803 9804 isActive : function() { 9805 return this.active; 9806 }, 9807 9808 setState : function(c, s) { 9809 var n = DOM.get(this.id); 9810 9811 c = this.classPrefix + c; 9812 9813 if (s) 9814 DOM.addClass(n, c); 9815 else 9816 DOM.removeClass(n, c); 9817 }, 9818 9819 isRendered : function() { 9820 return this.rendered; 9821 }, 9822 9823 renderHTML : function() { 9824 }, 9825 9826 renderTo : function(n) { 9827 DOM.setHTML(n, this.renderHTML()); 9828 }, 9829 9830 postRender : function() { 9831 var t = this, b; 9832 9833 // Set pending states 9834 if (is(t.disabled)) { 9835 b = t.disabled; 9836 t.disabled = -1; 9837 t.setDisabled(b); 9838 } 9839 9840 if (is(t.active)) { 9841 b = t.active; 9842 t.active = -1; 9843 t.setActive(b); 9844 } 9845 }, 9846 9847 remove : function() { 9848 DOM.remove(this.id); 9849 this.destroy(); 9850 }, 9851 9852 destroy : function() { 9853 tinymce.dom.Event.clear(this.id); 9854 } 9855 }); 9856 })(tinymce); 9857 tinymce.create('tinymce.ui.Container:tinymce.ui.Control', { 9858 Container : function(id, s, editor) { 9859 this.parent(id, s, editor); 9860 9861 this.controls = []; 9862 9863 this.lookup = {}; 9864 }, 9865 9866 add : function(c) { 9867 this.lookup[c.id] = c; 9868 this.controls.push(c); 9869 9870 return c; 9871 }, 9872 9873 get : function(n) { 9874 return this.lookup[n]; 9875 } 9876 }); 9877 9878 9879 tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { 9880 Separator : function(id, s) { 9881 this.parent(id, s); 9882 this.classPrefix = 'mceSeparator'; 9883 this.setDisabled(true); 9884 }, 9885 9886 renderHTML : function() { 9887 return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'}); 9888 } 9889 }); 9890 9891 (function(tinymce) { 9892 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk; 9893 9894 tinymce.create('tinymce.ui.MenuItem:tinymce.ui.Control', { 9895 MenuItem : function(id, s) { 9896 this.parent(id, s); 9897 this.classPrefix = 'mceMenuItem'; 9898 }, 9899 9900 setSelected : function(s) { 9901 this.setState('Selected', s); 9902 this.setAriaProperty('checked', !!s); 9903 this.selected = s; 9904 }, 9905 9906 isSelected : function() { 9907 return this.selected; 9908 }, 9909 9910 postRender : function() { 9911 var t = this; 9912 9913 t.parent(); 9914 9915 // Set pending state 9916 if (is(t.selected)) 9917 t.setSelected(t.selected); 9918 } 9919 }); 9920 })(tinymce); 9921 9922 (function(tinymce) { 9923 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk; 9924 9925 tinymce.create('tinymce.ui.Menu:tinymce.ui.MenuItem', { 9926 Menu : function(id, s) { 9927 var t = this; 9928 9929 t.parent(id, s); 9930 t.items = {}; 9931 t.collapsed = false; 9932 t.menuCount = 0; 9933 t.onAddItem = new tinymce.util.Dispatcher(this); 9934 }, 9935 9936 expand : function(d) { 9937 var t = this; 9938 9939 if (d) { 9940 walk(t, function(o) { 9941 if (o.expand) 9942 o.expand(); 9943 }, 'items', t); 9944 } 9945 9946 t.collapsed = false; 9947 }, 9948 9949 collapse : function(d) { 9950 var t = this; 9951 9952 if (d) { 9953 walk(t, function(o) { 9954 if (o.collapse) 9955 o.collapse(); 9956 }, 'items', t); 9957 } 9958 9959 t.collapsed = true; 9960 }, 9961 9962 isCollapsed : function() { 9963 return this.collapsed; 9964 }, 9965 9966 add : function(o) { 9967 if (!o.settings) 9968 o = new tinymce.ui.MenuItem(o.id || DOM.uniqueId(), o); 9969 9970 this.onAddItem.dispatch(this, o); 9971 9972 return this.items[o.id] = o; 9973 }, 9974 9975 addSeparator : function() { 9976 return this.add({separator : true}); 9977 }, 9978 9979 addMenu : function(o) { 9980 if (!o.collapse) 9981 o = this.createMenu(o); 9982 9983 this.menuCount++; 9984 9985 return this.add(o); 9986 }, 9987 9988 hasMenus : function() { 9989 return this.menuCount !== 0; 9990 }, 9991 9992 remove : function(o) { 9993 delete this.items[o.id]; 9994 }, 9995 9996 removeAll : function() { 9997 var t = this; 9998 9999 walk(t, function(o) { 10000 if (o.removeAll) 10001 o.removeAll(); 10002 else 10003 o.remove(); 10004 10005 o.destroy(); 10006 }, 'items', t); 10007 10008 t.items = {}; 10009 }, 10010 10011 createMenu : function(o) { 10012 var m = new tinymce.ui.Menu(o.id || DOM.uniqueId(), o); 10013 10014 m.onAddItem.add(this.onAddItem.dispatch, this.onAddItem); 10015 10016 return m; 10017 } 10018 }); 10019 })(tinymce); 10020 (function(tinymce) { 10021 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element; 10022 10023 tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', { 10024 DropMenu : function(id, s) { 10025 s = s || {}; 10026 s.container = s.container || DOM.doc.body; 10027 s.offset_x = s.offset_x || 0; 10028 s.offset_y = s.offset_y || 0; 10029 s.vp_offset_x = s.vp_offset_x || 0; 10030 s.vp_offset_y = s.vp_offset_y || 0; 10031 10032 if (is(s.icons) && !s.icons) 10033 s['class'] += ' mceNoIcons'; 10034 10035 this.parent(id, s); 10036 this.onShowMenu = new tinymce.util.Dispatcher(this); 10037 this.onHideMenu = new tinymce.util.Dispatcher(this); 10038 this.classPrefix = 'mceMenu'; 10039 }, 10040 10041 createMenu : function(s) { 10042 var t = this, cs = t.settings, m; 10043 10044 s.container = s.container || cs.container; 10045 s.parent = t; 10046 s.constrain = s.constrain || cs.constrain; 10047 s['class'] = s['class'] || cs['class']; 10048 s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x; 10049 s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y; 10050 s.keyboard_focus = cs.keyboard_focus; 10051 m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s); 10052 10053 m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem); 10054 10055 return m; 10056 }, 10057 10058 focus : function() { 10059 var t = this; 10060 if (t.keyboardNav) { 10061 t.keyboardNav.focus(); 10062 } 10063 }, 10064 10065 update : function() { 10066 var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th; 10067 10068 tw = s.max_width ? Math.min(tb.offsetWidth, s.max_width) : tb.offsetWidth; 10069 th = s.max_height ? Math.min(tb.offsetHeight, s.max_height) : tb.offsetHeight; 10070 10071 if (!DOM.boxModel) 10072 t.element.setStyles({width : tw + 2, height : th + 2}); 10073 else 10074 t.element.setStyles({width : tw, height : th}); 10075 10076 if (s.max_width) 10077 DOM.setStyle(co, 'width', tw); 10078 10079 if (s.max_height) { 10080 DOM.setStyle(co, 'height', th); 10081 10082 if (tb.clientHeight < s.max_height) 10083 DOM.setStyle(co, 'overflow', 'hidden'); 10084 } 10085 }, 10086 10087 showMenu : function(x, y, px) { 10088 var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix; 10089 10090 t.collapse(1); 10091 10092 if (t.isMenuVisible) 10093 return; 10094 10095 if (!t.rendered) { 10096 co = DOM.add(t.settings.container, t.renderNode()); 10097 10098 each(t.items, function(o) { 10099 o.postRender(); 10100 }); 10101 10102 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); 10103 } else 10104 co = DOM.get('menu_' + t.id); 10105 10106 // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug 10107 if (!tinymce.isOpera) 10108 DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF}); 10109 10110 DOM.show(co); 10111 t.update(); 10112 10113 x += s.offset_x || 0; 10114 y += s.offset_y || 0; 10115 vp.w -= 4; 10116 vp.h -= 4; 10117 10118 // Move inside viewport if not submenu 10119 if (s.constrain) { 10120 w = co.clientWidth - ot; 10121 h = co.clientHeight - ot; 10122 mx = vp.x + vp.w; 10123 my = vp.y + vp.h; 10124 10125 if ((x + s.vp_offset_x + w) > mx) 10126 x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w); 10127 10128 if ((y + s.vp_offset_y + h) > my) 10129 y = Math.max(0, (my - s.vp_offset_y) - h); 10130 } 10131 10132 DOM.setStyles(co, {left : x , top : y}); 10133 t.element.update(); 10134 10135 t.isMenuVisible = 1; 10136 t.mouseClickFunc = Event.add(co, 'click', function(e) { 10137 var m; 10138 10139 e = e.target; 10140 10141 if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) { 10142 m = t.items[e.id]; 10143 10144 if (m.isDisabled()) 10145 return; 10146 10147 dm = t; 10148 10149 while (dm) { 10150 if (dm.hideMenu) 10151 dm.hideMenu(); 10152 10153 dm = dm.settings.parent; 10154 } 10155 10156 if (m.settings.onclick) 10157 m.settings.onclick(e); 10158 10159 return false; // Cancel to fix onbeforeunload problem 10160 } 10161 }); 10162 10163 if (t.hasMenus()) { 10164 t.mouseOverFunc = Event.add(co, 'mouseover', function(e) { 10165 var m, r, mi; 10166 10167 e = e.target; 10168 if (e && (e = DOM.getParent(e, 'tr'))) { 10169 m = t.items[e.id]; 10170 10171 if (t.lastMenu) 10172 t.lastMenu.collapse(1); 10173 10174 if (m.isDisabled()) 10175 return; 10176 10177 if (e && DOM.hasClass(e, cp + 'ItemSub')) { 10178 //p = DOM.getPos(s.container); 10179 r = DOM.getRect(e); 10180 m.showMenu((r.x + r.w - ot), r.y - ot, r.x); 10181 t.lastMenu = m; 10182 DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive'); 10183 } 10184 } 10185 }); 10186 } 10187 10188 Event.add(co, 'keydown', t._keyHandler, t); 10189 10190 t.onShowMenu.dispatch(t); 10191 10192 if (s.keyboard_focus) { 10193 t._setupKeyboardNav(); 10194 } 10195 }, 10196 10197 hideMenu : function(c) { 10198 var t = this, co = DOM.get('menu_' + t.id), e; 10199 10200 if (!t.isMenuVisible) 10201 return; 10202 10203 if (t.keyboardNav) t.keyboardNav.destroy(); 10204 Event.remove(co, 'mouseover', t.mouseOverFunc); 10205 Event.remove(co, 'click', t.mouseClickFunc); 10206 Event.remove(co, 'keydown', t._keyHandler); 10207 DOM.hide(co); 10208 t.isMenuVisible = 0; 10209 10210 if (!c) 10211 t.collapse(1); 10212 10213 if (t.element) 10214 t.element.hide(); 10215 10216 if (e = DOM.get(t.id)) 10217 DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive'); 10218 10219 t.onHideMenu.dispatch(t); 10220 }, 10221 10222 add : function(o) { 10223 var t = this, co; 10224 10225 o = t.parent(o); 10226 10227 if (t.isRendered && (co = DOM.get('menu_' + t.id))) 10228 t._add(DOM.select('tbody', co)[0], o); 10229 10230 return o; 10231 }, 10232 10233 collapse : function(d) { 10234 this.parent(d); 10235 this.hideMenu(1); 10236 }, 10237 10238 remove : function(o) { 10239 DOM.remove(o.id); 10240 this.destroy(); 10241 10242 return this.parent(o); 10243 }, 10244 10245 destroy : function() { 10246 var t = this, co = DOM.get('menu_' + t.id); 10247 10248 if (t.keyboardNav) t.keyboardNav.destroy(); 10249 Event.remove(co, 'mouseover', t.mouseOverFunc); 10250 Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc); 10251 Event.remove(co, 'click', t.mouseClickFunc); 10252 Event.remove(co, 'keydown', t._keyHandler); 10253 10254 if (t.element) 10255 t.element.remove(); 10256 10257 DOM.remove(co); 10258 }, 10259 10260 renderNode : function() { 10261 var t = this, s = t.settings, n, tb, co, w; 10262 10263 w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'}); 10264 if (t.settings.parent) { 10265 DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id); 10266 } 10267 co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); 10268 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); 10269 10270 if (s.menu_line) 10271 DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'}); 10272 10273 // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'}); 10274 n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); 10275 tb = DOM.add(n, 'tbody'); 10276 10277 each(t.items, function(o) { 10278 t._add(tb, o); 10279 }); 10280 10281 t.rendered = true; 10282 10283 return w; 10284 }, 10285 10286 // Internal functions 10287 _setupKeyboardNav : function(){ 10288 var contextMenu, menuItems, t=this; 10289 contextMenu = DOM.get('menu_' + t.id); 10290 menuItems = DOM.select('a[role=option]', 'menu_' + t.id); 10291 menuItems.splice(0,0,contextMenu); 10292 t.keyboardNav = new tinymce.ui.KeyboardNavigation({ 10293 root: 'menu_' + t.id, 10294 items: menuItems, 10295 onCancel: function() { 10296 t.hideMenu(); 10297 }, 10298 enableUpDown: true 10299 }); 10300 contextMenu.focus(); 10301 }, 10302 10303 _keyHandler : function(evt) { 10304 var t = this, e; 10305 switch (evt.keyCode) { 10306 case 37: // Left 10307 if (t.settings.parent) { 10308 t.hideMenu(); 10309 t.settings.parent.focus(); 10310 Event.cancel(evt); 10311 } 10312 break; 10313 case 39: // Right 10314 if (t.mouseOverFunc) 10315 t.mouseOverFunc(evt); 10316 break; 10317 } 10318 }, 10319 10320 _add : function(tb, o) { 10321 var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic; 10322 10323 if (s.separator) { 10324 ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'}); 10325 DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'}); 10326 10327 if (n = ro.previousSibling) 10328 DOM.addClass(n, 'mceLast'); 10329 10330 return; 10331 } 10332 10333 n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'}); 10334 n = it = DOM.add(n, s.titleItem ? 'th' : 'td'); 10335 n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); 10336 10337 if (s.parent) { 10338 DOM.setAttrib(a, 'aria-haspopup', 'true'); 10339 DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id); 10340 } 10341 10342 DOM.addClass(it, s['class']); 10343 // n = DOM.add(n, 'span', {'class' : 'item'}); 10344 10345 ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')}); 10346 10347 if (s.icon_src) 10348 DOM.add(ic, 'img', {src : s.icon_src}); 10349 10350 n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title); 10351 10352 if (o.settings.style) { 10353 if (typeof o.settings.style == "function") 10354 o.settings.style = o.settings.style(); 10355 10356 DOM.setAttrib(n, 'style', o.settings.style); 10357 } 10358 10359 if (tb.childNodes.length == 1) 10360 DOM.addClass(ro, 'mceFirst'); 10361 10362 if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator')) 10363 DOM.addClass(ro, 'mceFirst'); 10364 10365 if (o.collapse) 10366 DOM.addClass(ro, cp + 'ItemSub'); 10367 10368 if (n = ro.previousSibling) 10369 DOM.removeClass(n, 'mceLast'); 10370 10371 DOM.addClass(ro, 'mceLast'); 10372 } 10373 }); 10374 })(tinymce); 10375 (function(tinymce) { 10376 var DOM = tinymce.DOM; 10377 10378 tinymce.create('tinymce.ui.Button:tinymce.ui.Control', { 10379 Button : function(id, s, ed) { 10380 this.parent(id, s, ed); 10381 this.classPrefix = 'mceButton'; 10382 }, 10383 10384 renderHTML : function() { 10385 var cp = this.classPrefix, s = this.settings, h, l; 10386 10387 l = DOM.encode(s.label || ''); 10388 h = '<a role="button" id="' + this.id + '" href="javascript:;" class="' + cp + ' ' + cp + 'Enabled ' + s['class'] + (l ? ' ' + cp + 'Labeled' : '') +'" onmousedown="return false;" onclick="return false;" aria-labelledby="' + this.id + '_voice" title="' + DOM.encode(s.title) + '">'; 10389 if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) 10390 h += '<span class="mceIcon ' + s['class'] + '"><img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" /></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); 10391 else 10392 h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); 10393 10394 h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>'; 10395 h += '</a>'; 10396 return h; 10397 }, 10398 10399 postRender : function() { 10400 var t = this, s = t.settings, imgBookmark; 10401 10402 // In IE a large image that occupies the entire editor area will be deselected when a button is clicked, so 10403 // need to keep the selection in case the selection is lost 10404 if (tinymce.isIE && t.editor) { 10405 tinymce.dom.Event.add(t.id, 'mousedown', function(e) { 10406 var nodeName = t.editor.selection.getNode().nodeName; 10407 imgBookmark = nodeName === 'IMG' ? t.editor.selection.getBookmark() : null; 10408 }); 10409 } 10410 tinymce.dom.Event.add(t.id, 'click', function(e) { 10411 if (!t.isDisabled()) { 10412 // restore the selection in case the selection is lost in IE 10413 if (tinymce.isIE && t.editor && imgBookmark !== null) { 10414 t.editor.selection.moveToBookmark(imgBookmark); 10415 } 10416 return s.onclick.call(s.scope, e); 10417 } 10418 }); 10419 tinymce.dom.Event.add(t.id, 'keyup', function(e) { 10420 if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR) 10421 return s.onclick.call(s.scope, e); 10422 }); 10423 } 10424 }); 10425 })(tinymce); 10426 10427 (function(tinymce) { 10428 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef; 10429 10430 tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', { 10431 ListBox : function(id, s, ed) { 10432 var t = this; 10433 10434 t.parent(id, s, ed); 10435 10436 t.items = []; 10437 10438 t.onChange = new Dispatcher(t); 10439 10440 t.onPostRender = new Dispatcher(t); 10441 10442 t.onAdd = new Dispatcher(t); 10443 10444 t.onRenderMenu = new tinymce.util.Dispatcher(this); 10445 10446 t.classPrefix = 'mceListBox'; 10447 t.marked = {}; 10448 }, 10449 10450 select : function(va) { 10451 var t = this, fv, f; 10452 10453 t.marked = {}; 10454 10455 if (va == undef) 10456 return t.selectByIndex(-1); 10457 10458 // Is string or number make function selector 10459 if (va && typeof(va)=="function") 10460 f = va; 10461 else { 10462 f = function(v) { 10463 return v == va; 10464 }; 10465 } 10466 10467 // Do we need to do something? 10468 if (va != t.selectedValue) { 10469 // Find item 10470 each(t.items, function(o, i) { 10471 if (f(o.value)) { 10472 fv = 1; 10473 t.selectByIndex(i); 10474 return false; 10475 } 10476 }); 10477 10478 if (!fv) 10479 t.selectByIndex(-1); 10480 } 10481 }, 10482 10483 selectByIndex : function(idx) { 10484 var t = this, e, o, label; 10485 10486 t.marked = {}; 10487 10488 if (idx != t.selectedIndex) { 10489 e = DOM.get(t.id + '_text'); 10490 label = DOM.get(t.id + '_voiceDesc'); 10491 o = t.items[idx]; 10492 10493 if (o) { 10494 t.selectedValue = o.value; 10495 t.selectedIndex = idx; 10496 DOM.setHTML(e, DOM.encode(o.title)); 10497 DOM.setHTML(label, t.settings.title + " - " + o.title); 10498 DOM.removeClass(e, 'mceTitle'); 10499 DOM.setAttrib(t.id, 'aria-valuenow', o.title); 10500 } else { 10501 DOM.setHTML(e, DOM.encode(t.settings.title)); 10502 DOM.setHTML(label, DOM.encode(t.settings.title)); 10503 DOM.addClass(e, 'mceTitle'); 10504 t.selectedValue = t.selectedIndex = null; 10505 DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title); 10506 } 10507 e = 0; 10508 } 10509 }, 10510 10511 mark : function(value) { 10512 this.marked[value] = true; 10513 }, 10514 10515 add : function(n, v, o) { 10516 var t = this; 10517 10518 o = o || {}; 10519 o = tinymce.extend(o, { 10520 title : n, 10521 value : v 10522 }); 10523 10524 t.items.push(o); 10525 t.onAdd.dispatch(t, o); 10526 }, 10527 10528 getLength : function() { 10529 return this.items.length; 10530 }, 10531 10532 renderHTML : function() { 10533 var h = '', t = this, s = t.settings, cp = t.classPrefix; 10534 10535 h = '<span role="listbox" aria-haspopup="true" aria-labelledby="' + t.id +'_voiceDesc" aria-describedby="' + t.id + '_voiceDesc"><table role="presentation" tabindex="0" id="' + t.id + '" cellpadding="0" cellspacing="0" class="' + cp + ' ' + cp + 'Enabled' + (s['class'] ? (' ' + s['class']) : '') + '"><tbody><tr>'; 10536 h += '<td>' + DOM.createHTML('span', {id: t.id + '_voiceDesc', 'class': 'voiceLabel', style:'display:none;'}, t.settings.title); 10537 h += DOM.createHTML('a', {id : t.id + '_text', tabindex : -1, href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '</td>'; 10538 h += '<td>' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '<span><span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span></span>') + '</td>'; 10539 h += '</tr></tbody></table></span>'; 10540 10541 return h; 10542 }, 10543 10544 showMenu : function() { 10545 var t = this, p2, e = DOM.get(this.id), m; 10546 10547 if (t.isDisabled() || t.items.length === 0) 10548 return; 10549 10550 if (t.menu && t.menu.isMenuVisible) 10551 return t.hideMenu(); 10552 10553 if (!t.isMenuRendered) { 10554 t.renderMenu(); 10555 t.isMenuRendered = true; 10556 } 10557 10558 p2 = DOM.getPos(e); 10559 10560 m = t.menu; 10561 m.settings.offset_x = p2.x; 10562 m.settings.offset_y = p2.y; 10563 m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus 10564 10565 // Select in menu 10566 each(t.items, function(o) { 10567 if (m.items[o.id]) { 10568 m.items[o.id].setSelected(0); 10569 } 10570 }); 10571 10572 each(t.items, function(o) { 10573 if (m.items[o.id] && t.marked[o.value]) { 10574 m.items[o.id].setSelected(1); 10575 } 10576 10577 if (o.value === t.selectedValue) { 10578 m.items[o.id].setSelected(1); 10579 } 10580 }); 10581 10582 m.showMenu(0, e.clientHeight); 10583 10584 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 10585 DOM.addClass(t.id, t.classPrefix + 'Selected'); 10586 10587 //DOM.get(t.id + '_text').focus(); 10588 }, 10589 10590 hideMenu : function(e) { 10591 var t = this; 10592 10593 if (t.menu && t.menu.isMenuVisible) { 10594 DOM.removeClass(t.id, t.classPrefix + 'Selected'); 10595 10596 // Prevent double toogles by canceling the mouse click event to the button 10597 if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open')) 10598 return; 10599 10600 if (!e || !DOM.getParent(e.target, '.mceMenu')) { 10601 DOM.removeClass(t.id, t.classPrefix + 'Selected'); 10602 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 10603 t.menu.hideMenu(); 10604 } 10605 } 10606 }, 10607 10608 renderMenu : function() { 10609 var t = this, m; 10610 10611 m = t.settings.control_manager.createDropMenu(t.id + '_menu', { 10612 menu_line : 1, 10613 'class' : t.classPrefix + 'Menu mceNoIcons', 10614 max_width : 250, 10615 max_height : 150 10616 }); 10617 10618 m.onHideMenu.add(function() { 10619 t.hideMenu(); 10620 t.focus(); 10621 }); 10622 10623 m.add({ 10624 title : t.settings.title, 10625 'class' : 'mceMenuItemTitle', 10626 onclick : function() { 10627 if (t.settings.onselect('') !== false) 10628 t.select(''); // Must be runned after 10629 } 10630 }); 10631 10632 each(t.items, function(o) { 10633 // No value then treat it as a title 10634 if (o.value === undef) { 10635 m.add({ 10636 title : o.title, 10637 role : "option", 10638 'class' : 'mceMenuItemTitle', 10639 onclick : function() { 10640 if (t.settings.onselect('') !== false) 10641 t.select(''); // Must be runned after 10642 } 10643 }); 10644 } else { 10645 o.id = DOM.uniqueId(); 10646 o.role= "option"; 10647 o.onclick = function() { 10648 if (t.settings.onselect(o.value) !== false) 10649 t.select(o.value); // Must be runned after 10650 }; 10651 10652 m.add(o); 10653 } 10654 }); 10655 10656 t.onRenderMenu.dispatch(t, m); 10657 t.menu = m; 10658 }, 10659 10660 postRender : function() { 10661 var t = this, cp = t.classPrefix; 10662 10663 Event.add(t.id, 'click', t.showMenu, t); 10664 Event.add(t.id, 'keydown', function(evt) { 10665 if (evt.keyCode == 32) { // Space 10666 t.showMenu(evt); 10667 Event.cancel(evt); 10668 } 10669 }); 10670 Event.add(t.id, 'focus', function() { 10671 if (!t._focused) { 10672 t.keyDownHandler = Event.add(t.id, 'keydown', function(e) { 10673 if (e.keyCode == 40) { 10674 t.showMenu(); 10675 Event.cancel(e); 10676 } 10677 }); 10678 t.keyPressHandler = Event.add(t.id, 'keypress', function(e) { 10679 var v; 10680 if (e.keyCode == 13) { 10681 // Fake select on enter 10682 v = t.selectedValue; 10683 t.selectedValue = null; // Needs to be null to fake change 10684 Event.cancel(e); 10685 t.settings.onselect(v); 10686 } 10687 }); 10688 } 10689 10690 t._focused = 1; 10691 }); 10692 Event.add(t.id, 'blur', function() { 10693 Event.remove(t.id, 'keydown', t.keyDownHandler); 10694 Event.remove(t.id, 'keypress', t.keyPressHandler); 10695 t._focused = 0; 10696 }); 10697 10698 // Old IE doesn't have hover on all elements 10699 if (tinymce.isIE6 || !DOM.boxModel) { 10700 Event.add(t.id, 'mouseover', function() { 10701 if (!DOM.hasClass(t.id, cp + 'Disabled')) 10702 DOM.addClass(t.id, cp + 'Hover'); 10703 }); 10704 10705 Event.add(t.id, 'mouseout', function() { 10706 if (!DOM.hasClass(t.id, cp + 'Disabled')) 10707 DOM.removeClass(t.id, cp + 'Hover'); 10708 }); 10709 } 10710 10711 t.onPostRender.dispatch(t, DOM.get(t.id)); 10712 }, 10713 10714 destroy : function() { 10715 this.parent(); 10716 10717 Event.clear(this.id + '_text'); 10718 Event.clear(this.id + '_open'); 10719 } 10720 }); 10721 })(tinymce); 10722 10723 (function(tinymce) { 10724 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef; 10725 10726 tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', { 10727 NativeListBox : function(id, s) { 10728 this.parent(id, s); 10729 this.classPrefix = 'mceNativeListBox'; 10730 }, 10731 10732 setDisabled : function(s) { 10733 DOM.get(this.id).disabled = s; 10734 this.setAriaProperty('disabled', s); 10735 }, 10736 10737 isDisabled : function() { 10738 return DOM.get(this.id).disabled; 10739 }, 10740 10741 select : function(va) { 10742 var t = this, fv, f; 10743 10744 if (va == undef) 10745 return t.selectByIndex(-1); 10746 10747 // Is string or number make function selector 10748 if (va && typeof(va)=="function") 10749 f = va; 10750 else { 10751 f = function(v) { 10752 return v == va; 10753 }; 10754 } 10755 10756 // Do we need to do something? 10757 if (va != t.selectedValue) { 10758 // Find item 10759 each(t.items, function(o, i) { 10760 if (f(o.value)) { 10761 fv = 1; 10762 t.selectByIndex(i); 10763 return false; 10764 } 10765 }); 10766 10767 if (!fv) 10768 t.selectByIndex(-1); 10769 } 10770 }, 10771 10772 selectByIndex : function(idx) { 10773 DOM.get(this.id).selectedIndex = idx + 1; 10774 this.selectedValue = this.items[idx] ? this.items[idx].value : null; 10775 }, 10776 10777 add : function(n, v, a) { 10778 var o, t = this; 10779 10780 a = a || {}; 10781 a.value = v; 10782 10783 if (t.isRendered()) 10784 DOM.add(DOM.get(this.id), 'option', a, n); 10785 10786 o = { 10787 title : n, 10788 value : v, 10789 attribs : a 10790 }; 10791 10792 t.items.push(o); 10793 t.onAdd.dispatch(t, o); 10794 }, 10795 10796 getLength : function() { 10797 return this.items.length; 10798 }, 10799 10800 renderHTML : function() { 10801 var h, t = this; 10802 10803 h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --'); 10804 10805 each(t.items, function(it) { 10806 h += DOM.createHTML('option', {value : it.value}, it.title); 10807 }); 10808 10809 h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h); 10810 h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title); 10811 return h; 10812 }, 10813 10814 postRender : function() { 10815 var t = this, ch, changeListenerAdded = true; 10816 10817 t.rendered = true; 10818 10819 function onChange(e) { 10820 var v = t.items[e.target.selectedIndex - 1]; 10821 10822 if (v && (v = v.value)) { 10823 t.onChange.dispatch(t, v); 10824 10825 if (t.settings.onselect) 10826 t.settings.onselect(v); 10827 } 10828 }; 10829 10830 Event.add(t.id, 'change', onChange); 10831 10832 // Accessibility keyhandler 10833 Event.add(t.id, 'keydown', function(e) { 10834 var bf; 10835 10836 Event.remove(t.id, 'change', ch); 10837 changeListenerAdded = false; 10838 10839 bf = Event.add(t.id, 'blur', function() { 10840 if (changeListenerAdded) return; 10841 changeListenerAdded = true; 10842 Event.add(t.id, 'change', onChange); 10843 Event.remove(t.id, 'blur', bf); 10844 }); 10845 10846 //prevent default left and right keys on chrome - so that the keyboard navigation is used. 10847 if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) { 10848 return Event.prevent(e); 10849 } 10850 10851 if (e.keyCode == 13 || e.keyCode == 32) { 10852 onChange(e); 10853 return Event.cancel(e); 10854 } 10855 }); 10856 10857 t.onPostRender.dispatch(t, DOM.get(t.id)); 10858 } 10859 }); 10860 })(tinymce); 10861 10862 (function(tinymce) { 10863 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; 10864 10865 tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', { 10866 MenuButton : function(id, s, ed) { 10867 this.parent(id, s, ed); 10868 10869 this.onRenderMenu = new tinymce.util.Dispatcher(this); 10870 10871 s.menu_container = s.menu_container || DOM.doc.body; 10872 }, 10873 10874 showMenu : function() { 10875 var t = this, p1, p2, e = DOM.get(t.id), m; 10876 10877 if (t.isDisabled()) 10878 return; 10879 10880 if (!t.isMenuRendered) { 10881 t.renderMenu(); 10882 t.isMenuRendered = true; 10883 } 10884 10885 if (t.isMenuVisible) 10886 return t.hideMenu(); 10887 10888 p1 = DOM.getPos(t.settings.menu_container); 10889 p2 = DOM.getPos(e); 10890 10891 m = t.menu; 10892 m.settings.offset_x = p2.x; 10893 m.settings.offset_y = p2.y; 10894 m.settings.vp_offset_x = p2.x; 10895 m.settings.vp_offset_y = p2.y; 10896 m.settings.keyboard_focus = t._focused; 10897 m.showMenu(0, e.firstChild.clientHeight); 10898 10899 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 10900 t.setState('Selected', 1); 10901 10902 t.isMenuVisible = 1; 10903 }, 10904 10905 renderMenu : function() { 10906 var t = this, m; 10907 10908 m = t.settings.control_manager.createDropMenu(t.id + '_menu', { 10909 menu_line : 1, 10910 'class' : this.classPrefix + 'Menu', 10911 icons : t.settings.icons 10912 }); 10913 10914 m.onHideMenu.add(function() { 10915 t.hideMenu(); 10916 t.focus(); 10917 }); 10918 10919 t.onRenderMenu.dispatch(t, m); 10920 t.menu = m; 10921 }, 10922 10923 hideMenu : function(e) { 10924 var t = this; 10925 10926 // Prevent double toogles by canceling the mouse click event to the button 10927 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';})) 10928 return; 10929 10930 if (!e || !DOM.getParent(e.target, '.mceMenu')) { 10931 t.setState('Selected', 0); 10932 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 10933 if (t.menu) 10934 t.menu.hideMenu(); 10935 } 10936 10937 t.isMenuVisible = 0; 10938 }, 10939 10940 postRender : function() { 10941 var t = this, s = t.settings; 10942 10943 Event.add(t.id, 'click', function() { 10944 if (!t.isDisabled()) { 10945 if (s.onclick) 10946 s.onclick(t.value); 10947 10948 t.showMenu(); 10949 } 10950 }); 10951 } 10952 }); 10953 })(tinymce); 10954 10955 (function(tinymce) { 10956 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; 10957 10958 tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', { 10959 SplitButton : function(id, s, ed) { 10960 this.parent(id, s, ed); 10961 this.classPrefix = 'mceSplitButton'; 10962 }, 10963 10964 renderHTML : function() { 10965 var h, t = this, s = t.settings, h1; 10966 10967 h = '<tbody><tr>'; 10968 10969 if (s.image) 10970 h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']}); 10971 else 10972 h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, ''); 10973 10974 h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title); 10975 h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_action', tabindex: '-1', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>'; 10976 10977 h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, '<span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span>'); 10978 h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>'; 10979 10980 h += '</tr></tbody>'; 10981 h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h); 10982 return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h); 10983 }, 10984 10985 postRender : function() { 10986 var t = this, s = t.settings, activate; 10987 10988 if (s.onclick) { 10989 activate = function(evt) { 10990 if (!t.isDisabled()) { 10991 s.onclick(t.value); 10992 Event.cancel(evt); 10993 } 10994 }; 10995 Event.add(t.id + '_action', 'click', activate); 10996 Event.add(t.id, ['click', 'keydown'], function(evt) { 10997 var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40; 10998 if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) { 10999 activate(); 11000 Event.cancel(evt); 11001 } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) { 11002 t.showMenu(); 11003 Event.cancel(evt); 11004 } 11005 }); 11006 } 11007 11008 Event.add(t.id + '_open', 'click', function (evt) { 11009 t.showMenu(); 11010 Event.cancel(evt); 11011 }); 11012 Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;}); 11013 Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;}); 11014 11015 // Old IE doesn't have hover on all elements 11016 if (tinymce.isIE6 || !DOM.boxModel) { 11017 Event.add(t.id, 'mouseover', function() { 11018 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) 11019 DOM.addClass(t.id, 'mceSplitButtonHover'); 11020 }); 11021 11022 Event.add(t.id, 'mouseout', function() { 11023 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) 11024 DOM.removeClass(t.id, 'mceSplitButtonHover'); 11025 }); 11026 } 11027 }, 11028 11029 destroy : function() { 11030 this.parent(); 11031 11032 Event.clear(this.id + '_action'); 11033 Event.clear(this.id + '_open'); 11034 Event.clear(this.id); 11035 } 11036 }); 11037 })(tinymce); 11038 11039 (function(tinymce) { 11040 var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each; 11041 11042 tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', { 11043 ColorSplitButton : function(id, s, ed) { 11044 var t = this; 11045 11046 t.parent(id, s, ed); 11047 11048 t.settings = s = tinymce.extend({ 11049 colors : '000000,993300,333300,003300,003366,000080,333399,333333,800000,FF6600,808000,008000,008080,0000FF,666699,808080,FF0000,FF9900,99CC00,339966,33CCCC,3366FF,800080,999999,FF00FF,FFCC00,FFFF00,00FF00,00FFFF,00CCFF,993366,C0C0C0,FF99CC,FFCC99,FFFF99,CCFFCC,CCFFFF,99CCFF,CC99FF,FFFFFF', 11050 grid_width : 8, 11051 default_color : '#888888' 11052 }, t.settings); 11053 11054 t.onShowMenu = new tinymce.util.Dispatcher(t); 11055 11056 t.onHideMenu = new tinymce.util.Dispatcher(t); 11057 11058 t.value = s.default_color; 11059 }, 11060 11061 showMenu : function() { 11062 var t = this, r, p, e, p2; 11063 11064 if (t.isDisabled()) 11065 return; 11066 11067 if (!t.isMenuRendered) { 11068 t.renderMenu(); 11069 t.isMenuRendered = true; 11070 } 11071 11072 if (t.isMenuVisible) 11073 return t.hideMenu(); 11074 11075 e = DOM.get(t.id); 11076 DOM.show(t.id + '_menu'); 11077 DOM.addClass(e, 'mceSplitButtonSelected'); 11078 p2 = DOM.getPos(e); 11079 DOM.setStyles(t.id + '_menu', { 11080 left : p2.x, 11081 top : p2.y + e.firstChild.clientHeight, 11082 zIndex : 200000 11083 }); 11084 e = 0; 11085 11086 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 11087 t.onShowMenu.dispatch(t); 11088 11089 if (t._focused) { 11090 t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) { 11091 if (e.keyCode == 27) 11092 t.hideMenu(); 11093 }); 11094 11095 DOM.select('a', t.id + '_menu')[0].focus(); // Select first link 11096 } 11097 11098 t.keyboardNav = new tinymce.ui.KeyboardNavigation({ 11099 root: t.id + '_menu', 11100 items: DOM.select('a', t.id + '_menu'), 11101 onCancel: function() { 11102 t.hideMenu(); 11103 t.focus(); 11104 } 11105 }); 11106 11107 t.keyboardNav.focus(); 11108 t.isMenuVisible = 1; 11109 }, 11110 11111 hideMenu : function(e) { 11112 var t = this; 11113 11114 if (t.isMenuVisible) { 11115 // Prevent double toogles by canceling the mouse click event to the button 11116 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) 11117 return; 11118 11119 if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { 11120 DOM.removeClass(t.id, 'mceSplitButtonSelected'); 11121 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 11122 Event.remove(t.id + '_menu', 'keydown', t._keyHandler); 11123 DOM.hide(t.id + '_menu'); 11124 } 11125 11126 t.isMenuVisible = 0; 11127 t.onHideMenu.dispatch(); 11128 t.keyboardNav.destroy(); 11129 } 11130 }, 11131 11132 renderMenu : function() { 11133 var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context; 11134 11135 w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s.menu_class + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); 11136 m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'}); 11137 DOM.add(m, 'span', {'class' : 'mceMenuLine'}); 11138 11139 n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'}); 11140 tb = DOM.add(n, 'tbody'); 11141 11142 // Generate color grid 11143 i = 0; 11144 each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) { 11145 c = c.replace(/^#/, ''); 11146 11147 if (!i--) { 11148 tr = DOM.add(tb, 'tr'); 11149 i = s.grid_width - 1; 11150 } 11151 11152 n = DOM.add(tr, 'td'); 11153 var settings = { 11154 href : 'javascript:;', 11155 style : { 11156 backgroundColor : '#' + c 11157 }, 11158 'title': t.editor.getLang('colors.' + c, c), 11159 'data-mce-color' : '#' + c 11160 }; 11161 11162 // adding a proper ARIA role = button causes JAWS to read things incorrectly on IE. 11163 if (!tinymce.isIE ) { 11164 settings.role = 'option'; 11165 } 11166 11167 n = DOM.add(n, 'a', settings); 11168 11169 if (t.editor.forcedHighContrastMode) { 11170 n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' }); 11171 if (n.getContext && (context = n.getContext("2d"))) { 11172 context.fillStyle = '#' + c; 11173 context.fillRect(0, 0, 16, 16); 11174 } else { 11175 // No point leaving a canvas element around if it's not supported for drawing on anyway. 11176 DOM.remove(n); 11177 } 11178 } 11179 }); 11180 11181 if (s.more_colors_func) { 11182 n = DOM.add(tb, 'tr'); 11183 n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'}); 11184 n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); 11185 11186 Event.add(n, 'click', function(e) { 11187 s.more_colors_func.call(s.more_colors_scope || this); 11188 return Event.cancel(e); // Cancel to fix onbeforeunload problem 11189 }); 11190 } 11191 11192 DOM.addClass(m, 'mceColorSplitMenu'); 11193 11194 // Prevent IE from scrolling and hindering click to occur #4019 11195 Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);}); 11196 11197 Event.add(t.id + '_menu', 'click', function(e) { 11198 var c; 11199 11200 e = DOM.getParent(e.target, 'a', tb); 11201 11202 if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color'))) 11203 t.setColor(c); 11204 11205 return false; // Prevent IE auto save warning 11206 }); 11207 11208 return w; 11209 }, 11210 11211 setColor : function(c) { 11212 this.displayColor(c); 11213 this.hideMenu(); 11214 this.settings.onselect(c); 11215 }, 11216 11217 displayColor : function(c) { 11218 var t = this; 11219 11220 DOM.setStyle(t.id + '_preview', 'backgroundColor', c); 11221 11222 t.value = c; 11223 }, 11224 11225 postRender : function() { 11226 var t = this, id = t.id; 11227 11228 t.parent(); 11229 DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'}); 11230 DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value); 11231 }, 11232 11233 destroy : function() { 11234 var self = this; 11235 11236 self.parent(); 11237 11238 Event.clear(self.id + '_menu'); 11239 Event.clear(self.id + '_more'); 11240 DOM.remove(self.id + '_menu'); 11241 11242 if (self.keyboardNav) { 11243 self.keyboardNav.destroy(); 11244 } 11245 } 11246 }); 11247 })(tinymce); 11248 11249 (function(tinymce) { 11250 // Shorten class names 11251 var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event; 11252 tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', { 11253 renderHTML : function() { 11254 var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings; 11255 11256 h.push('<div id="' + t.id + '" role="group" aria-labelledby="' + t.id + '_voice">'); 11257 //TODO: ACC test this out - adding a role = application for getting the landmarks working well. 11258 h.push("<span role='application'>"); 11259 h.push('<span id="' + t.id + '_voice" class="mceVoiceLabel" style="display:none;">' + dom.encode(settings.name) + '</span>'); 11260 each(controls, function(toolbar) { 11261 h.push(toolbar.renderHTML()); 11262 }); 11263 h.push("</span>"); 11264 h.push('</div>'); 11265 11266 return h.join(''); 11267 }, 11268 11269 focus : function() { 11270 var t = this; 11271 dom.get(t.id).focus(); 11272 }, 11273 11274 postRender : function() { 11275 var t = this, items = []; 11276 11277 each(t.controls, function(toolbar) { 11278 each (toolbar.controls, function(control) { 11279 if (control.id) { 11280 items.push(control); 11281 } 11282 }); 11283 }); 11284 11285 t.keyNav = new tinymce.ui.KeyboardNavigation({ 11286 root: t.id, 11287 items: items, 11288 onCancel: function() { 11289 //Move focus if webkit so that navigation back will read the item. 11290 if (tinymce.isWebKit) { 11291 dom.get(t.editor.id+"_ifr").focus(); 11292 } 11293 t.editor.focus(); 11294 }, 11295 excludeFromTabOrder: !t.settings.tab_focus_toolbar 11296 }); 11297 }, 11298 11299 destroy : function() { 11300 var self = this; 11301 11302 self.parent(); 11303 self.keyNav.destroy(); 11304 Event.clear(self.id); 11305 } 11306 }); 11307 })(tinymce); 11308 11309 (function(tinymce) { 11310 // Shorten class names 11311 var dom = tinymce.DOM, each = tinymce.each; 11312 tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 11313 renderHTML : function() { 11314 var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl; 11315 11316 cl = t.controls; 11317 for (i=0; i<cl.length; i++) { 11318 // Get current control, prev control, next control and if the control is a list box or not 11319 co = cl[i]; 11320 pr = cl[i - 1]; 11321 nx = cl[i + 1]; 11322 11323 // Add toolbar start 11324 if (i === 0) { 11325 c = 'mceToolbarStart'; 11326 11327 if (co.Button) 11328 c += ' mceToolbarStartButton'; 11329 else if (co.SplitButton) 11330 c += ' mceToolbarStartSplitButton'; 11331 else if (co.ListBox) 11332 c += ' mceToolbarStartListBox'; 11333 11334 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->')); 11335 } 11336 11337 // Add toolbar end before list box and after the previous button 11338 // This is to fix the o2k7 editor skins 11339 if (pr && co.ListBox) { 11340 if (pr.Button || pr.SplitButton) 11341 h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '<!-- IE -->')); 11342 } 11343 11344 // Render control HTML 11345 11346 // IE 8 quick fix, needed to propertly generate a hit area for anchors 11347 if (dom.stdMode) 11348 h += '<td style="position: relative">' + co.renderHTML() + '</td>'; 11349 else 11350 h += '<td>' + co.renderHTML() + '</td>'; 11351 11352 // Add toolbar start after list box and before the next button 11353 // This is to fix the o2k7 editor skins 11354 if (nx && co.ListBox) { 11355 if (nx.Button || nx.SplitButton) 11356 h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '<!-- IE -->')); 11357 } 11358 } 11359 11360 c = 'mceToolbarEnd'; 11361 11362 if (co.Button) 11363 c += ' mceToolbarEndButton'; 11364 else if (co.SplitButton) 11365 c += ' mceToolbarEndSplitButton'; 11366 else if (co.ListBox) 11367 c += ' mceToolbarEndListBox'; 11368 11369 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->')); 11370 11371 return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || '', role: 'presentation', tabindex: '-1'}, '<tbody><tr>' + h + '</tr></tbody>'); 11372 } 11373 }); 11374 })(tinymce); 11375 11376 (function(tinymce) { 11377 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each; 11378 11379 tinymce.create('tinymce.AddOnManager', { 11380 AddOnManager : function() { 11381 var self = this; 11382 11383 self.items = []; 11384 self.urls = {}; 11385 self.lookup = {}; 11386 self.onAdd = new Dispatcher(self); 11387 }, 11388 11389 get : function(n) { 11390 if (this.lookup[n]) { 11391 return this.lookup[n].instance; 11392 } else { 11393 return undefined; 11394 } 11395 }, 11396 11397 dependencies : function(n) { 11398 var result; 11399 if (this.lookup[n]) { 11400 result = this.lookup[n].dependencies; 11401 } 11402 return result || []; 11403 }, 11404 11405 requireLangPack : function(n) { 11406 var s = tinymce.settings; 11407 11408 if (s && s.language && s.language_load !== false) 11409 tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js'); 11410 }, 11411 11412 add : function(id, o, dependencies) { 11413 this.items.push(o); 11414 this.lookup[id] = {instance:o, dependencies:dependencies}; 11415 this.onAdd.dispatch(this, id, o); 11416 11417 return o; 11418 }, 11419 createUrl: function(baseUrl, dep) { 11420 if (typeof dep === "object") { 11421 return dep 11422 } else { 11423 return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; 11424 } 11425 }, 11426 11427 addComponents: function(pluginName, scripts) { 11428 var pluginUrl = this.urls[pluginName]; 11429 tinymce.each(scripts, function(script){ 11430 tinymce.ScriptLoader.add(pluginUrl+"/"+script); 11431 }); 11432 }, 11433 11434 load : function(n, u, cb, s) { 11435 var t = this, url = u; 11436 11437 function loadDependencies() { 11438 var dependencies = t.dependencies(n); 11439 tinymce.each(dependencies, function(dep) { 11440 var newUrl = t.createUrl(u, dep); 11441 t.load(newUrl.resource, newUrl, undefined, undefined); 11442 }); 11443 if (cb) { 11444 if (s) { 11445 cb.call(s); 11446 } else { 11447 cb.call(tinymce.ScriptLoader); 11448 } 11449 } 11450 } 11451 11452 if (t.urls[n]) 11453 return; 11454 if (typeof u === "object") 11455 url = u.prefix + u.resource + u.suffix; 11456 11457 if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) 11458 url = tinymce.baseURL + '/' + url; 11459 11460 t.urls[n] = url.substring(0, url.lastIndexOf('/')); 11461 11462 if (t.lookup[n]) { 11463 loadDependencies(); 11464 } else { 11465 tinymce.ScriptLoader.add(url, loadDependencies, s); 11466 } 11467 } 11468 }); 11469 11470 // Create plugin and theme managers 11471 tinymce.PluginManager = new tinymce.AddOnManager(); 11472 tinymce.ThemeManager = new tinymce.AddOnManager(); 11473 }(tinymce)); 11474 11475 (function(tinymce) { 11476 // Shorten names 11477 var each = tinymce.each, extend = tinymce.extend, 11478 DOM = tinymce.DOM, Event = tinymce.dom.Event, 11479 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, 11480 explode = tinymce.explode, 11481 Dispatcher = tinymce.util.Dispatcher, undef, instanceCounter = 0; 11482 11483 // Setup some URLs where the editor API is located and where the document is 11484 tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); 11485 if (!/[\/\\]$/.test(tinymce.documentBaseURL)) 11486 tinymce.documentBaseURL += '/'; 11487 11488 tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL); 11489 11490 tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL); 11491 11492 // Add before unload listener 11493 // This was required since IE was leaking memory if you added and removed beforeunload listeners 11494 // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event 11495 tinymce.onBeforeUnload = new Dispatcher(tinymce); 11496 11497 // Must be on window or IE will leak if the editor is placed in frame or iframe 11498 Event.add(window, 'beforeunload', function(e) { 11499 tinymce.onBeforeUnload.dispatch(tinymce, e); 11500 }); 11501 11502 tinymce.onAddEditor = new Dispatcher(tinymce); 11503 11504 tinymce.onRemoveEditor = new Dispatcher(tinymce); 11505 11506 tinymce.EditorManager = extend(tinymce, { 11507 editors : [], 11508 11509 i18n : {}, 11510 11511 activeEditor : null, 11512 11513 init : function(s) { 11514 var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed; 11515 11516 function createId(elm) { 11517 var id = elm.id; 11518 11519 // Use element id, or unique name or generate a unique id 11520 if (!id) { 11521 id = elm.name; 11522 11523 if (id && !DOM.get(id)) { 11524 id = elm.name; 11525 } else { 11526 // Generate unique name 11527 id = DOM.uniqueId(); 11528 } 11529 11530 elm.setAttribute('id', id); 11531 } 11532 11533 return id; 11534 }; 11535 11536 function execCallback(se, n, s) { 11537 var f = se[n]; 11538 11539 if (!f) 11540 return; 11541 11542 if (tinymce.is(f, 'string')) { 11543 s = f.replace(/\.\w+$/, ''); 11544 s = s ? tinymce.resolve(s) : 0; 11545 f = tinymce.resolve(f); 11546 } 11547 11548 return f.apply(s || this, Array.prototype.slice.call(arguments, 2)); 11549 }; 11550 11551 function hasClass(n, c) { 11552 return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); 11553 }; 11554 11555 t.settings = s; 11556 11557 // Legacy call 11558 Event.bind(window, 'ready', function() { 11559 var l, co; 11560 11561 execCallback(s, 'onpageload'); 11562 11563 switch (s.mode) { 11564 case "exact": 11565 l = s.elements || ''; 11566 11567 if(l.length > 0) { 11568 each(explode(l), function(v) { 11569 if (DOM.get(v)) { 11570 ed = new tinymce.Editor(v, s); 11571 el.push(ed); 11572 ed.render(1); 11573 } else { 11574 each(document.forms, function(f) { 11575 each(f.elements, function(e) { 11576 if (e.name === v) { 11577 v = 'mce_editor_' + instanceCounter++; 11578 DOM.setAttrib(e, 'id', v); 11579 11580 ed = new tinymce.Editor(v, s); 11581 el.push(ed); 11582 ed.render(1); 11583 } 11584 }); 11585 }); 11586 } 11587 }); 11588 } 11589 break; 11590 11591 case "textareas": 11592 case "specific_textareas": 11593 each(DOM.select('textarea'), function(elm) { 11594 if (s.editor_deselector && hasClass(elm, s.editor_deselector)) 11595 return; 11596 11597 if (!s.editor_selector || hasClass(elm, s.editor_selector)) { 11598 ed = new tinymce.Editor(createId(elm), s); 11599 el.push(ed); 11600 ed.render(1); 11601 } 11602 }); 11603 break; 11604 11605 default: 11606 if (s.types) { 11607 // Process type specific selector 11608 each(s.types, function(type) { 11609 each(DOM.select(type.selector), function(elm) { 11610 var editor = new tinymce.Editor(createId(elm), tinymce.extend({}, s, type)); 11611 el.push(editor); 11612 editor.render(1); 11613 }); 11614 }); 11615 } else if (s.selector) { 11616 // Process global selector 11617 each(DOM.select(s.selector), function(elm) { 11618 var editor = new tinymce.Editor(createId(elm), s); 11619 el.push(editor); 11620 editor.render(1); 11621 }); 11622 } 11623 } 11624 11625 // Call onInit when all editors are initialized 11626 if (s.oninit) { 11627 l = co = 0; 11628 11629 each(el, function(ed) { 11630 co++; 11631 11632 if (!ed.initialized) { 11633 // Wait for it 11634 ed.onInit.add(function() { 11635 l++; 11636 11637 // All done 11638 if (l == co) 11639 execCallback(s, 'oninit'); 11640 }); 11641 } else 11642 l++; 11643 11644 // All done 11645 if (l == co) 11646 execCallback(s, 'oninit'); 11647 }); 11648 } 11649 }); 11650 }, 11651 11652 get : function(id) { 11653 if (id === undef) 11654 return this.editors; 11655 11656 return this.editors[id]; 11657 }, 11658 11659 getInstanceById : function(id) { 11660 return this.get(id); 11661 }, 11662 11663 add : function(editor) { 11664 var self = this, editors = self.editors; 11665 11666 // Add named and index editor instance 11667 editors[editor.id] = editor; 11668 editors.push(editor); 11669 11670 self._setActive(editor); 11671 self.onAddEditor.dispatch(self, editor); 11672 11673 11674 // Patch the tinymce.Editor instance with jQuery adapter logic 11675 if (tinymce.adapter) 11676 tinymce.adapter.patchEditor(editor); 11677 11678 11679 return editor; 11680 }, 11681 11682 remove : function(editor) { 11683 var t = this, i, editors = t.editors; 11684 11685 // Not in the collection 11686 if (!editors[editor.id]) 11687 return null; 11688 11689 delete editors[editor.id]; 11690 11691 for (i = 0; i < editors.length; i++) { 11692 if (editors[i] == editor) { 11693 editors.splice(i, 1); 11694 break; 11695 } 11696 } 11697 11698 // Select another editor since the active one was removed 11699 if (t.activeEditor == editor) 11700 t._setActive(editors[0]); 11701 11702 editor.destroy(); 11703 t.onRemoveEditor.dispatch(t, editor); 11704 11705 return editor; 11706 }, 11707 11708 execCommand : function(c, u, v) { 11709 var t = this, ed = t.get(v), w; 11710 11711 function clr() { 11712 ed.destroy(); 11713 w.detachEvent('onunload', clr); 11714 w = w.tinyMCE = w.tinymce = null; // IE leak 11715 }; 11716 11717 // Manager commands 11718 switch (c) { 11719 case "mceFocus": 11720 ed.focus(); 11721 return true; 11722 11723 case "mceAddEditor": 11724 case "mceAddControl": 11725 if (!t.get(v)) 11726 new tinymce.Editor(v, t.settings).render(); 11727 11728 return true; 11729 11730 case "mceAddFrameControl": 11731 w = v.window; 11732 11733 // Add tinyMCE global instance and tinymce namespace to specified window 11734 w.tinyMCE = tinyMCE; 11735 w.tinymce = tinymce; 11736 11737 tinymce.DOM.doc = w.document; 11738 tinymce.DOM.win = w; 11739 11740 ed = new tinymce.Editor(v.element_id, v); 11741 ed.render(); 11742 11743 // Fix IE memory leaks 11744 if (tinymce.isIE) { 11745 w.attachEvent('onunload', clr); 11746 } 11747 11748 v.page_window = null; 11749 11750 return true; 11751 11752 case "mceRemoveEditor": 11753 case "mceRemoveControl": 11754 if (ed) 11755 ed.remove(); 11756 11757 return true; 11758 11759 case 'mceToggleEditor': 11760 if (!ed) { 11761 t.execCommand('mceAddControl', 0, v); 11762 return true; 11763 } 11764 11765 if (ed.isHidden()) 11766 ed.show(); 11767 else 11768 ed.hide(); 11769 11770 return true; 11771 } 11772 11773 // Run command on active editor 11774 if (t.activeEditor) 11775 return t.activeEditor.execCommand(c, u, v); 11776 11777 return false; 11778 }, 11779 11780 execInstanceCommand : function(id, c, u, v) { 11781 var ed = this.get(id); 11782 11783 if (ed) 11784 return ed.execCommand(c, u, v); 11785 11786 return false; 11787 }, 11788 11789 triggerSave : function() { 11790 each(this.editors, function(e) { 11791 e.save(); 11792 }); 11793 }, 11794 11795 addI18n : function(p, o) { 11796 var lo, i18n = this.i18n; 11797 11798 if (!tinymce.is(p, 'string')) { 11799 each(p, function(o, lc) { 11800 each(o, function(o, g) { 11801 each(o, function(o, k) { 11802 if (g === 'common') 11803 i18n[lc + '.' + k] = o; 11804 else 11805 i18n[lc + '.' + g + '.' + k] = o; 11806 }); 11807 }); 11808 }); 11809 } else { 11810 each(o, function(o, k) { 11811 i18n[p + '.' + k] = o; 11812 }); 11813 } 11814 }, 11815 11816 // Private methods 11817 11818 _setActive : function(editor) { 11819 this.selectedInstance = this.activeEditor = editor; 11820 } 11821 }); 11822 })(tinymce); 11823 11824 (function(tinymce) { 11825 // Shorten these names 11826 var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend, 11827 each = tinymce.each, isGecko = tinymce.isGecko, 11828 isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is, 11829 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, 11830 explode = tinymce.explode; 11831 11832 tinymce.create('tinymce.Editor', { 11833 Editor : function(id, settings) { 11834 var self = this, TRUE = true; 11835 11836 self.settings = settings = extend({ 11837 id : id, 11838 language : 'en', 11839 theme : 'advanced', 11840 skin : 'default', 11841 delta_width : 0, 11842 delta_height : 0, 11843 popup_css : '', 11844 plugins : '', 11845 document_base_url : tinymce.documentBaseURL, 11846 add_form_submit_trigger : TRUE, 11847 submit_patch : TRUE, 11848 add_unload_trigger : TRUE, 11849 convert_urls : TRUE, 11850 relative_urls : TRUE, 11851 remove_script_host : TRUE, 11852 table_inline_editing : false, 11853 object_resizing : TRUE, 11854 accessibility_focus : TRUE, 11855 doctype : tinymce.isIE6 ? '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">' : '<!DOCTYPE>', // Use old doctype on IE 6 to avoid horizontal scroll 11856 visual : TRUE, 11857 font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large', 11858 font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size 11859 apply_source_formatting : TRUE, 11860 directionality : 'ltr', 11861 forced_root_block : 'p', 11862 hidden_input : TRUE, 11863 padd_empty_editor : TRUE, 11864 render_ui : TRUE, 11865 indentation : '30px', 11866 fix_table_elements : TRUE, 11867 inline_styles : TRUE, 11868 convert_fonts_to_spans : TRUE, 11869 indent : 'simple', 11870 indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 11871 indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 11872 validate : TRUE, 11873 entity_encoding : 'named', 11874 url_converter : self.convertURL, 11875 url_converter_scope : self, 11876 ie7_compat : TRUE 11877 }, settings); 11878 11879 self.id = self.editorId = id; 11880 11881 self.isNotDirty = false; 11882 11883 self.plugins = {}; 11884 11885 self.documentBaseURI = new tinymce.util.URI(settings.document_base_url || tinymce.documentBaseURL, { 11886 base_uri : tinyMCE.baseURI 11887 }); 11888 11889 self.baseURI = tinymce.baseURI; 11890 11891 self.contentCSS = []; 11892 11893 self.contentStyles = []; 11894 11895 // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic 11896 self.setupEvents(); 11897 11898 // Internal command handler objects 11899 self.execCommands = {}; 11900 self.queryStateCommands = {}; 11901 self.queryValueCommands = {}; 11902 11903 // Call setup 11904 self.execCallback('setup', self); 11905 }, 11906 11907 render : function(nst) { 11908 var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader; 11909 11910 // Page is not loaded yet, wait for it 11911 if (!Event.domLoaded) { 11912 Event.add(window, 'ready', function() { 11913 t.render(); 11914 }); 11915 return; 11916 } 11917 11918 tinyMCE.settings = s; 11919 11920 // Element not found, then skip initialization 11921 if (!t.getElement()) 11922 return; 11923 11924 // Is a iPad/iPhone and not on iOS5, then skip initialization. We need to sniff 11925 // here since the browser says it has contentEditable support but there is no visible caret. 11926 if (tinymce.isIDevice && !tinymce.isIOS5) 11927 return; 11928 11929 // Add hidden input for non input elements inside form elements 11930 if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form')) 11931 DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id); 11932 11933 // Hide target element early to prevent content flashing 11934 if (!s.content_editable) { 11935 t.orgVisibility = t.getElement().style.visibility; 11936 t.getElement().style.visibility = 'hidden'; 11937 } 11938 11939 if (tinymce.WindowManager) 11940 t.windowManager = new tinymce.WindowManager(t); 11941 11942 if (s.encoding == 'xml') { 11943 t.onGetContent.add(function(ed, o) { 11944 if (o.save) 11945 o.content = DOM.encode(o.content); 11946 }); 11947 } 11948 11949 if (s.add_form_submit_trigger) { 11950 t.onSubmit.addToTop(function() { 11951 if (t.initialized) { 11952 t.save(); 11953 t.isNotDirty = 1; 11954 } 11955 }); 11956 } 11957 11958 if (s.add_unload_trigger) { 11959 t._beforeUnload = tinyMCE.onBeforeUnload.add(function() { 11960 if (t.initialized && !t.destroyed && !t.isHidden()) 11961 t.save({format : 'raw', no_events : true}); 11962 }); 11963 } 11964 11965 tinymce.addUnload(t.destroy, t); 11966 11967 if (s.submit_patch) { 11968 t.onBeforeRenderUI.add(function() { 11969 var n = t.getElement().form; 11970 11971 if (!n) 11972 return; 11973 11974 // Already patched 11975 if (n._mceOldSubmit) 11976 return; 11977 11978 // Check page uses id="submit" or name="submit" for it's submit button 11979 if (!n.submit.nodeType && !n.submit.length) { 11980 t.formElement = n; 11981 n._mceOldSubmit = n.submit; 11982 n.submit = function() { 11983 // Save all instances 11984 tinymce.triggerSave(); 11985 t.isNotDirty = 1; 11986 11987 return t.formElement._mceOldSubmit(t.formElement); 11988 }; 11989 } 11990 11991 n = null; 11992 }); 11993 } 11994 11995 // Load scripts 11996 function loadScripts() { 11997 if (s.language && s.language_load !== false) 11998 sl.add(tinymce.baseURL + '/langs/' + s.language + '.js'); 11999 12000 if (s.theme && typeof s.theme != "function" && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme]) 12001 ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js'); 12002 12003 each(explode(s.plugins), function(p) { 12004 if (p &&!PluginManager.urls[p]) { 12005 if (p.charAt(0) == '-') { 12006 p = p.substr(1, p.length); 12007 var dependencies = PluginManager.dependencies(p); 12008 each(dependencies, function(dep) { 12009 var defaultSettings = {prefix:'plugins/', resource: dep, suffix:'/editor_plugin' + tinymce.suffix + '.js'}; 12010 dep = PluginManager.createUrl(defaultSettings, dep); 12011 PluginManager.load(dep.resource, dep); 12012 }); 12013 } else { 12014 // Skip safari plugin, since it is removed as of 3.3b1 12015 if (p == 'safari') { 12016 return; 12017 } 12018 PluginManager.load(p, {prefix:'plugins/', resource: p, suffix:'/editor_plugin' + tinymce.suffix + '.js'}); 12019 } 12020 } 12021 }); 12022 12023 // Init when que is loaded 12024 sl.loadQueue(function() { 12025 if (!t.removed) 12026 t.init(); 12027 }); 12028 }; 12029 12030 loadScripts(); 12031 }, 12032 12033 init : function() { 12034 var n, t = this, s = t.settings, w, h, mh, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; 12035 12036 tinymce.add(t); 12037 12038 s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area')); 12039 12040 if (s.theme) { 12041 if (typeof s.theme != "function") { 12042 s.theme = s.theme.replace(/-/, ''); 12043 o = ThemeManager.get(s.theme); 12044 t.theme = new o(); 12045 12046 if (t.theme.init) 12047 t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); 12048 } else { 12049 t.theme = s.theme; 12050 } 12051 } 12052 12053 function initPlugin(p) { 12054 var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po; 12055 if (c && tinymce.inArray(initializedPlugins,p) === -1) { 12056 each(PluginManager.dependencies(p), function(dep){ 12057 initPlugin(dep); 12058 }); 12059 po = new c(t, u); 12060 12061 t.plugins[p] = po; 12062 12063 if (po.init) { 12064 po.init(t, u); 12065 initializedPlugins.push(p); 12066 } 12067 } 12068 } 12069 12070 // Create all plugins 12071 each(explode(s.plugins.replace(/\-/g, '')), initPlugin); 12072 12073 // Setup popup CSS path(s) 12074 if (s.popup_css !== false) { 12075 if (s.popup_css) 12076 s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css); 12077 else 12078 s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css"); 12079 } 12080 12081 if (s.popup_css_add) 12082 s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add); 12083 12084 t.controlManager = new tinymce.ControlManager(t); 12085 12086 // Enables users to override the control factory 12087 t.onBeforeRenderUI.dispatch(t, t.controlManager); 12088 12089 // Measure box 12090 if (s.render_ui && t.theme) { 12091 t.orgDisplay = e.style.display; 12092 12093 if (typeof s.theme != "function") { 12094 w = s.width || e.style.width || e.offsetWidth; 12095 h = s.height || e.style.height || e.offsetHeight; 12096 mh = s.min_height || 100; 12097 re = /^[0-9\.]+(|px)$/i; 12098 12099 if (re.test('' + w)) 12100 w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100); 12101 12102 if (re.test('' + h)) 12103 h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), mh); 12104 12105 // Render UI 12106 o = t.theme.renderUI({ 12107 targetNode : e, 12108 width : w, 12109 height : h, 12110 deltaWidth : s.delta_width, 12111 deltaHeight : s.delta_height 12112 }); 12113 12114 // Resize editor 12115 DOM.setStyles(o.sizeContainer || o.editorContainer, { 12116 width : w, 12117 height : h 12118 }); 12119 12120 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); 12121 if (h < mh) 12122 h = mh; 12123 } else { 12124 o = s.theme(t, e); 12125 12126 // Convert element type to id:s 12127 if (o.editorContainer.nodeType) { 12128 o.editorContainer = o.editorContainer.id = o.editorContainer.id || t.id + "_parent"; 12129 } 12130 12131 // Convert element type to id:s 12132 if (o.iframeContainer.nodeType) { 12133 o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || t.id + "_iframecontainer"; 12134 } 12135 12136 // Use specified iframe height or the targets offsetHeight 12137 h = o.iframeHeight || e.offsetHeight; 12138 12139 // Store away the selection when it's changed to it can be restored later with a editor.focus() call 12140 if (isIE) { 12141 t.onInit.add(function(ed) { 12142 ed.dom.bind(ed.getBody(), 'beforedeactivate keydown', function() { 12143 ed.lastIERng = ed.selection.getRng(); 12144 }); 12145 }); 12146 } 12147 } 12148 12149 t.editorContainer = o.editorContainer; 12150 } 12151 12152 // Load specified content CSS last 12153 if (s.content_css) { 12154 each(explode(s.content_css), function(u) { 12155 t.contentCSS.push(t.documentBaseURI.toAbsolute(u)); 12156 }); 12157 } 12158 12159 // Content editable mode ends here 12160 if (s.content_editable) { 12161 e = n = o = null; // Fix IE leak 12162 return t.initContentBody(); 12163 } 12164 12165 // User specified a document.domain value 12166 if (document.domain && location.hostname != document.domain) 12167 tinymce.relaxedDomain = document.domain; 12168 12169 t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">'; 12170 12171 // We only need to override paths if we have to 12172 // IE has a bug where it remove site absolute urls to relative ones if this is specified 12173 if (s.document_base_url != tinymce.documentBaseURL) 12174 t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />'; 12175 12176 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. 12177 if (s.ie7_compat) 12178 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; 12179 else 12180 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'; 12181 12182 t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; 12183 12184 // Load the CSS by injecting them into the HTML this will reduce "flicker" 12185 for (i = 0; i < t.contentCSS.length; i++) { 12186 t.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + t.contentCSS[i] + '" />'; 12187 } 12188 12189 t.contentCSS = []; 12190 12191 bi = s.body_id || 'tinymce'; 12192 if (bi.indexOf('=') != -1) { 12193 bi = t.getParam('body_id', '', 'hash'); 12194 bi = bi[t.id] || bi; 12195 } 12196 12197 bc = s.body_class || ''; 12198 if (bc.indexOf('=') != -1) { 12199 bc = t.getParam('body_class', '', 'hash'); 12200 bc = bc[t.id] || ''; 12201 } 12202 12203 t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '" onload="window.parent.tinyMCE.get(\'' + t.id + '\').onLoad.dispatch();"><br></body></html>'; 12204 12205 // Domain relaxing enabled, then set document domain 12206 if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) { 12207 // We need to write the contents here in IE since multiple writes messes up refresh button and back button 12208 u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.initContentBody();})()'; 12209 } 12210 12211 // Create iframe 12212 // TODO: ACC add the appropriate description on this. 12213 n = DOM.add(o.iframeContainer, 'iframe', { 12214 id : t.id + "_ifr", 12215 src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7 12216 frameBorder : '0', 12217 allowTransparency : "true", 12218 title : s.aria_label, 12219 style : { 12220 width : '100%', 12221 height : h, 12222 display : 'block' // Important for Gecko to render the iframe correctly 12223 } 12224 }); 12225 12226 t.contentAreaContainer = o.iframeContainer; 12227 12228 if (o.editorContainer) { 12229 DOM.get(o.editorContainer).style.display = t.orgDisplay; 12230 } 12231 12232 // Restore visibility on target element 12233 e.style.visibility = t.orgVisibility; 12234 12235 DOM.get(t.id).style.display = 'none'; 12236 DOM.setAttrib(t.id, 'aria-hidden', true); 12237 12238 if (!tinymce.relaxedDomain || !u) 12239 t.initContentBody(); 12240 12241 e = n = o = null; // Cleanup 12242 }, 12243 12244 initContentBody : function() { 12245 var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body, contentCssText; 12246 12247 // Setup iframe body 12248 if ((!isIE || !tinymce.relaxedDomain) && !settings.content_editable) { 12249 doc.open(); 12250 doc.write(self.iframeHTML); 12251 doc.close(); 12252 12253 if (tinymce.relaxedDomain) 12254 doc.domain = tinymce.relaxedDomain; 12255 } 12256 12257 if (settings.content_editable) { 12258 DOM.addClass(targetElm, 'mceContentBody'); 12259 self.contentDocument = doc = settings.content_document || document; 12260 self.contentWindow = settings.content_window || window; 12261 self.bodyElement = targetElm; 12262 12263 // Prevent leak in IE 12264 settings.content_document = settings.content_window = null; 12265 } 12266 12267 // It will not steal focus while setting contentEditable 12268 body = self.getBody(); 12269 body.disabled = true; 12270 12271 if (!settings.readonly) 12272 body.contentEditable = self.getParam('content_editable_state', true); 12273 12274 body.disabled = false; 12275 12276 self.schema = new tinymce.html.Schema(settings); 12277 12278 self.dom = new tinymce.dom.DOMUtils(doc, { 12279 keep_values : true, 12280 url_converter : self.convertURL, 12281 url_converter_scope : self, 12282 hex_colors : settings.force_hex_style_colors, 12283 class_filter : settings.class_filter, 12284 update_styles : true, 12285 root_element : settings.content_editable ? self.id : null, 12286 schema : self.schema 12287 }); 12288 12289 self.parser = new tinymce.html.DomParser(settings, self.schema); 12290 12291 // Convert src and href into data-mce-src, data-mce-href and data-mce-style 12292 self.parser.addAttributeFilter('src,href,style', function(nodes, name) { 12293 var i = nodes.length, node, dom = self.dom, value, internalName; 12294 12295 while (i--) { 12296 node = nodes[i]; 12297 value = node.attr(name); 12298 internalName = 'data-mce-' + name; 12299 12300 // Add internal attribute if we need to we don't on a refresh of the document 12301 if (!node.attributes.map[internalName]) { 12302 if (name === "style") 12303 node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); 12304 else 12305 node.attr(internalName, self.convertURL(value, name, node.name)); 12306 } 12307 } 12308 }); 12309 12310 // Keep scripts from executing 12311 self.parser.addNodeFilter('script', function(nodes, name) { 12312 var i = nodes.length, node; 12313 12314 while (i--) { 12315 node = nodes[i]; 12316 node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); 12317 } 12318 }); 12319 12320 self.parser.addNodeFilter('#cdata', function(nodes, name) { 12321 var i = nodes.length, node; 12322 12323 while (i--) { 12324 node = nodes[i]; 12325 node.type = 8; 12326 node.name = '#comment'; 12327 node.value = '[CDATA[' + node.value + ']]'; 12328 } 12329 }); 12330 12331 self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) { 12332 var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements(); 12333 12334 while (i--) { 12335 node = nodes[i]; 12336 12337 if (node.isEmpty(nonEmptyElements)) 12338 node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true; 12339 } 12340 }); 12341 12342 self.serializer = new tinymce.dom.Serializer(settings, self.dom, self.schema); 12343 12344 self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer, self); 12345 12346 self.formatter = new tinymce.Formatter(self); 12347 12348 self.undoManager = new tinymce.UndoManager(self); 12349 12350 self.forceBlocks = new tinymce.ForceBlocks(self); 12351 self.enterKey = new tinymce.EnterKey(self); 12352 self.editorCommands = new tinymce.EditorCommands(self); 12353 12354 self.onExecCommand.add(function(editor, command) { 12355 // Don't refresh the select lists until caret move 12356 if (!/^(FontName|FontSize)$/.test(command)) 12357 self.nodeChanged(); 12358 }); 12359 12360 // Pass through 12361 self.serializer.onPreProcess.add(function(se, o) { 12362 return self.onPreProcess.dispatch(self, o, se); 12363 }); 12364 12365 self.serializer.onPostProcess.add(function(se, o) { 12366 return self.onPostProcess.dispatch(self, o, se); 12367 }); 12368 12369 self.onPreInit.dispatch(self); 12370 12371 if (!settings.browser_spellcheck && !settings.gecko_spellcheck) 12372 doc.body.spellcheck = false; 12373 12374 if (!settings.readonly) { 12375 self.bindNativeEvents(); 12376 } 12377 12378 self.controlManager.onPostRender.dispatch(self, self.controlManager); 12379 self.onPostRender.dispatch(self); 12380 12381 self.quirks = tinymce.util.Quirks(self); 12382 12383 if (settings.directionality) 12384 body.dir = settings.directionality; 12385 12386 if (settings.nowrap) 12387 body.style.whiteSpace = "nowrap"; 12388 12389 if (settings.protect) { 12390 self.onBeforeSetContent.add(function(ed, o) { 12391 each(settings.protect, function(pattern) { 12392 o.content = o.content.replace(pattern, function(str) { 12393 return '<!--mce:protected ' + escape(str) + '-->'; 12394 }); 12395 }); 12396 }); 12397 } 12398 12399 // Add visual aids when new contents is added 12400 self.onSetContent.add(function() { 12401 self.addVisual(self.getBody()); 12402 }); 12403 12404 // Remove empty contents 12405 if (settings.padd_empty_editor) { 12406 self.onPostProcess.add(function(ed, o) { 12407 o.content = o.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, ''); 12408 }); 12409 } 12410 12411 self.load({initial : true, format : 'html'}); 12412 self.startContent = self.getContent({format : 'raw'}); 12413 12414 self.initialized = true; 12415 12416 self.onInit.dispatch(self); 12417 self.execCallback('setupcontent_callback', self.id, body, doc); 12418 self.execCallback('init_instance_callback', self); 12419 self.focus(true); 12420 self.nodeChanged({initial : true}); 12421 12422 // Add editor specific CSS styles 12423 if (self.contentStyles.length > 0) { 12424 contentCssText = ''; 12425 12426 each(self.contentStyles, function(style) { 12427 contentCssText += style + "\r\n"; 12428 }); 12429 12430 self.dom.addStyle(contentCssText); 12431 } 12432 12433 // Load specified content CSS last 12434 each(self.contentCSS, function(url) { 12435 self.dom.loadCSS(url); 12436 }); 12437 12438 // Handle auto focus 12439 if (settings.auto_focus) { 12440 setTimeout(function () { 12441 var ed = tinymce.get(settings.auto_focus); 12442 12443 ed.selection.select(ed.getBody(), 1); 12444 ed.selection.collapse(1); 12445 ed.getBody().focus(); 12446 ed.getWin().focus(); 12447 }, 100); 12448 } 12449 12450 // Clean up references for IE 12451 targetElm = doc = body = null; 12452 }, 12453 12454 focus : function(skip_focus) { 12455 var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, ieRng, controlElm, doc = self.getDoc(), body; 12456 12457 if (!skip_focus) { 12458 if (self.lastIERng) { 12459 selection.setRng(self.lastIERng); 12460 } 12461 12462 // Get selected control element 12463 ieRng = selection.getRng(); 12464 if (ieRng.item) { 12465 controlElm = ieRng.item(0); 12466 } 12467 12468 self._refreshContentEditable(); 12469 12470 // Focus the window iframe 12471 if (!contentEditable) { 12472 self.getWin().focus(); 12473 } 12474 12475 // Focus the body as well since it's contentEditable 12476 if (tinymce.isGecko || contentEditable) { 12477 body = self.getBody(); 12478 12479 // Check for setActive since it doesn't scroll to the element 12480 if (body.setActive) { 12481 body.setActive(); 12482 } else { 12483 body.focus(); 12484 } 12485 12486 if (contentEditable) { 12487 selection.normalize(); 12488 } 12489 } 12490 12491 // Restore selected control element 12492 // This is needed when for example an image is selected within a 12493 // layer a call to focus will then remove the control selection 12494 if (controlElm && controlElm.ownerDocument == doc) { 12495 ieRng = doc.body.createControlRange(); 12496 ieRng.addElement(controlElm); 12497 ieRng.select(); 12498 } 12499 } 12500 12501 if (tinymce.activeEditor != self) { 12502 if ((oed = tinymce.activeEditor) != null) 12503 oed.onDeactivate.dispatch(oed, self); 12504 12505 self.onActivate.dispatch(self, oed); 12506 } 12507 12508 tinymce._setActive(self); 12509 }, 12510 12511 execCallback : function(n) { 12512 var t = this, f = t.settings[n], s; 12513 12514 if (!f) 12515 return; 12516 12517 // Look through lookup 12518 if (t.callbackLookup && (s = t.callbackLookup[n])) { 12519 f = s.func; 12520 s = s.scope; 12521 } 12522 12523 if (is(f, 'string')) { 12524 s = f.replace(/\.\w+$/, ''); 12525 s = s ? tinymce.resolve(s) : 0; 12526 f = tinymce.resolve(f); 12527 t.callbackLookup = t.callbackLookup || {}; 12528 t.callbackLookup[n] = {func : f, scope : s}; 12529 } 12530 12531 return f.apply(s || t, Array.prototype.slice.call(arguments, 1)); 12532 }, 12533 12534 translate : function(s) { 12535 var c = this.settings.language || 'en', i18n = tinymce.i18n; 12536 12537 if (!s) 12538 return ''; 12539 12540 return i18n[c + '.' + s] || s.replace(/\{\#([^\}]+)\}/g, function(a, b) { 12541 return i18n[c + '.' + b] || '{#' + b + '}'; 12542 }); 12543 }, 12544 12545 getLang : function(n, dv) { 12546 return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}'); 12547 }, 12548 12549 getParam : function(n, dv, ty) { 12550 var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o; 12551 12552 if (ty === 'hash') { 12553 o = {}; 12554 12555 if (is(v, 'string')) { 12556 each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) { 12557 v = v.split('='); 12558 12559 if (v.length > 1) 12560 o[tr(v[0])] = tr(v[1]); 12561 else 12562 o[tr(v[0])] = tr(v); 12563 }); 12564 } else 12565 o = v; 12566 12567 return o; 12568 } 12569 12570 return v; 12571 }, 12572 12573 nodeChanged : function(o) { 12574 var self = this, selection = self.selection, node; 12575 12576 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading 12577 if (self.initialized) { 12578 o = o || {}; 12579 12580 // Get start node 12581 node = selection.getStart() || self.getBody(); 12582 node = isIE && node.ownerDocument != self.getDoc() ? self.getBody() : node; // Fix for IE initial state 12583 12584 // Get parents and add them to object 12585 o.parents = []; 12586 self.dom.getParent(node, function(node) { 12587 if (node.nodeName == 'BODY') 12588 return true; 12589 12590 o.parents.push(node); 12591 }); 12592 12593 self.onNodeChange.dispatch( 12594 self, 12595 o ? o.controlManager || self.controlManager : self.controlManager, 12596 node, 12597 selection.isCollapsed(), 12598 o 12599 ); 12600 } 12601 }, 12602 12603 addButton : function(name, settings) { 12604 var self = this; 12605 12606 self.buttons = self.buttons || {}; 12607 self.buttons[name] = settings; 12608 }, 12609 12610 addCommand : function(name, callback, scope) { 12611 this.execCommands[name] = {func : callback, scope : scope || this}; 12612 }, 12613 12614 addQueryStateHandler : function(name, callback, scope) { 12615 this.queryStateCommands[name] = {func : callback, scope : scope || this}; 12616 }, 12617 12618 addQueryValueHandler : function(name, callback, scope) { 12619 this.queryValueCommands[name] = {func : callback, scope : scope || this}; 12620 }, 12621 12622 addShortcut : function(pa, desc, cmd_func, sc) { 12623 var t = this, c; 12624 12625 if (t.settings.custom_shortcuts === false) 12626 return false; 12627 12628 t.shortcuts = t.shortcuts || {}; 12629 12630 if (is(cmd_func, 'string')) { 12631 c = cmd_func; 12632 12633 cmd_func = function() { 12634 t.execCommand(c, false, null); 12635 }; 12636 } 12637 12638 if (is(cmd_func, 'object')) { 12639 c = cmd_func; 12640 12641 cmd_func = function() { 12642 t.execCommand(c[0], c[1], c[2]); 12643 }; 12644 } 12645 12646 each(explode(pa), function(pa) { 12647 var o = { 12648 func : cmd_func, 12649 scope : sc || this, 12650 desc : t.translate(desc), 12651 alt : false, 12652 ctrl : false, 12653 shift : false 12654 }; 12655 12656 each(explode(pa, '+'), function(v) { 12657 switch (v) { 12658 case 'alt': 12659 case 'ctrl': 12660 case 'shift': 12661 o[v] = true; 12662 break; 12663 12664 default: 12665 o.charCode = v.charCodeAt(0); 12666 o.keyCode = v.toUpperCase().charCodeAt(0); 12667 } 12668 }); 12669 12670 t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o; 12671 }); 12672 12673 return true; 12674 }, 12675 12676 execCommand : function(cmd, ui, val, a) { 12677 var t = this, s = 0, o, st; 12678 12679 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus)) 12680 t.focus(); 12681 12682 a = extend({}, a); 12683 t.onBeforeExecCommand.dispatch(t, cmd, ui, val, a); 12684 if (a.terminate) 12685 return false; 12686 12687 // Command callback 12688 if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) { 12689 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12690 return true; 12691 } 12692 12693 // Registred commands 12694 if (o = t.execCommands[cmd]) { 12695 st = o.func.call(o.scope, ui, val); 12696 12697 // Fall through on true 12698 if (st !== true) { 12699 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12700 return st; 12701 } 12702 } 12703 12704 // Plugin commands 12705 each(t.plugins, function(p) { 12706 if (p.execCommand && p.execCommand(cmd, ui, val)) { 12707 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12708 s = 1; 12709 return false; 12710 } 12711 }); 12712 12713 if (s) 12714 return true; 12715 12716 // Theme commands 12717 if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) { 12718 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12719 return true; 12720 } 12721 12722 // Editor commands 12723 if (t.editorCommands.execCommand(cmd, ui, val)) { 12724 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12725 return true; 12726 } 12727 12728 // Browser commands 12729 t.getDoc().execCommand(cmd, ui, val); 12730 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12731 }, 12732 12733 queryCommandState : function(cmd) { 12734 var t = this, o, s; 12735 12736 // Is hidden then return undefined 12737 if (t._isHidden()) 12738 return; 12739 12740 // Registred commands 12741 if (o = t.queryStateCommands[cmd]) { 12742 s = o.func.call(o.scope); 12743 12744 // Fall though on true 12745 if (s !== true) 12746 return s; 12747 } 12748 12749 // Registred commands 12750 o = t.editorCommands.queryCommandState(cmd); 12751 if (o !== -1) 12752 return o; 12753 12754 // Browser commands 12755 try { 12756 return this.getDoc().queryCommandState(cmd); 12757 } catch (ex) { 12758 // Fails sometimes see bug: 1896577 12759 } 12760 }, 12761 12762 queryCommandValue : function(c) { 12763 var t = this, o, s; 12764 12765 // Is hidden then return undefined 12766 if (t._isHidden()) 12767 return; 12768 12769 // Registred commands 12770 if (o = t.queryValueCommands[c]) { 12771 s = o.func.call(o.scope); 12772 12773 // Fall though on true 12774 if (s !== true) 12775 return s; 12776 } 12777 12778 // Registred commands 12779 o = t.editorCommands.queryCommandValue(c); 12780 if (is(o)) 12781 return o; 12782 12783 // Browser commands 12784 try { 12785 return this.getDoc().queryCommandValue(c); 12786 } catch (ex) { 12787 // Fails sometimes see bug: 1896577 12788 } 12789 }, 12790 12791 show : function() { 12792 var self = this; 12793 12794 DOM.show(self.getContainer()); 12795 DOM.hide(self.id); 12796 self.load(); 12797 }, 12798 12799 hide : function() { 12800 var self = this, doc = self.getDoc(); 12801 12802 // Fixed bug where IE has a blinking cursor left from the editor 12803 if (isIE && doc) 12804 doc.execCommand('SelectAll'); 12805 12806 // We must save before we hide so Safari doesn't crash 12807 self.save(); 12808 DOM.hide(self.getContainer()); 12809 DOM.setStyle(self.id, 'display', self.orgDisplay); 12810 }, 12811 12812 isHidden : function() { 12813 return !DOM.isHidden(this.id); 12814 }, 12815 12816 setProgressState : function(b, ti, o) { 12817 this.onSetProgressState.dispatch(this, b, ti, o); 12818 12819 return b; 12820 }, 12821 12822 load : function(o) { 12823 var t = this, e = t.getElement(), h; 12824 12825 if (e) { 12826 o = o || {}; 12827 o.load = true; 12828 12829 // Double encode existing entities in the value 12830 h = t.setContent(is(e.value) ? e.value : e.innerHTML, o); 12831 o.element = e; 12832 12833 if (!o.no_events) 12834 t.onLoadContent.dispatch(t, o); 12835 12836 o.element = e = null; 12837 12838 return h; 12839 } 12840 }, 12841 12842 save : function(o) { 12843 var t = this, e = t.getElement(), h, f; 12844 12845 if (!e || !t.initialized) 12846 return; 12847 12848 o = o || {}; 12849 o.save = true; 12850 12851 o.element = e; 12852 h = o.content = t.getContent(o); 12853 12854 if (!o.no_events) 12855 t.onSaveContent.dispatch(t, o); 12856 12857 h = o.content; 12858 12859 if (!/TEXTAREA|INPUT/i.test(e.nodeName)) { 12860 e.innerHTML = h; 12861 12862 // Update hidden form element 12863 if (f = DOM.getParent(t.id, 'form')) { 12864 each(f.elements, function(e) { 12865 if (e.name == t.id) { 12866 e.value = h; 12867 return false; 12868 } 12869 }); 12870 } 12871 } else 12872 e.value = h; 12873 12874 o.element = e = null; 12875 12876 return h; 12877 }, 12878 12879 setContent : function(content, args) { 12880 var self = this, rootNode, body = self.getBody(), forcedRootBlockName; 12881 12882 // Setup args object 12883 args = args || {}; 12884 args.format = args.format || 'html'; 12885 args.set = true; 12886 args.content = content; 12887 12888 // Do preprocessing 12889 if (!args.no_events) 12890 self.onBeforeSetContent.dispatch(self, args); 12891 12892 content = args.content; 12893 12894 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content 12895 // It will also be impossible to place the caret in the editor unless there is a BR element present 12896 if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) { 12897 forcedRootBlockName = self.settings.forced_root_block; 12898 if (forcedRootBlockName) 12899 content = '<' + forcedRootBlockName + '><br data-mce-bogus="1"></' + forcedRootBlockName + '>'; 12900 else 12901 content = '<br data-mce-bogus="1">'; 12902 12903 body.innerHTML = content; 12904 self.selection.select(body, true); 12905 self.selection.collapse(true); 12906 return; 12907 } 12908 12909 // Parse and serialize the html 12910 if (args.format !== 'raw') { 12911 content = new tinymce.html.Serializer({}, self.schema).serialize( 12912 self.parser.parse(content) 12913 ); 12914 } 12915 12916 // Set the new cleaned contents to the editor 12917 args.content = tinymce.trim(content); 12918 self.dom.setHTML(body, args.content); 12919 12920 // Do post processing 12921 if (!args.no_events) 12922 self.onSetContent.dispatch(self, args); 12923 12924 // Don't normalize selection if the focused element isn't the body in content editable mode since it will steal focus otherwise 12925 if (!self.settings.content_editable || document.activeElement === self.getBody()) { 12926 self.selection.normalize(); 12927 } 12928 12929 return args.content; 12930 }, 12931 12932 getContent : function(args) { 12933 var self = this, content; 12934 12935 // Setup args object 12936 args = args || {}; 12937 args.format = args.format || 'html'; 12938 args.get = true; 12939 args.getInner = true; 12940 12941 // Do preprocessing 12942 if (!args.no_events) 12943 self.onBeforeGetContent.dispatch(self, args); 12944 12945 // Get raw contents or by default the cleaned contents 12946 if (args.format == 'raw') 12947 content = self.getBody().innerHTML; 12948 else 12949 content = self.serializer.serialize(self.getBody(), args); 12950 12951 args.content = tinymce.trim(content); 12952 12953 // Do post processing 12954 if (!args.no_events) 12955 self.onGetContent.dispatch(self, args); 12956 12957 return args.content; 12958 }, 12959 12960 isDirty : function() { 12961 var self = this; 12962 12963 return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty; 12964 }, 12965 12966 getContainer : function() { 12967 var self = this; 12968 12969 if (!self.container) 12970 self.container = DOM.get(self.editorContainer || self.id + '_parent'); 12971 12972 return self.container; 12973 }, 12974 12975 getContentAreaContainer : function() { 12976 return this.contentAreaContainer; 12977 }, 12978 12979 getElement : function() { 12980 return DOM.get(this.settings.content_element || this.id); 12981 }, 12982 12983 getWin : function() { 12984 var self = this, elm; 12985 12986 if (!self.contentWindow) { 12987 elm = DOM.get(self.id + "_ifr"); 12988 12989 if (elm) 12990 self.contentWindow = elm.contentWindow; 12991 } 12992 12993 return self.contentWindow; 12994 }, 12995 12996 getDoc : function() { 12997 var self = this, win; 12998 12999 if (!self.contentDocument) { 13000 win = self.getWin(); 13001 13002 if (win) 13003 self.contentDocument = win.document; 13004 } 13005 13006 return self.contentDocument; 13007 }, 13008 13009 getBody : function() { 13010 return this.bodyElement || this.getDoc().body; 13011 }, 13012 13013 convertURL : function(url, name, elm) { 13014 var self = this, settings = self.settings; 13015 13016 // Use callback instead 13017 if (settings.urlconverter_callback) 13018 return self.execCallback('urlconverter_callback', url, elm, true, name); 13019 13020 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs 13021 if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0) 13022 return url; 13023 13024 // Convert to relative 13025 if (settings.relative_urls) 13026 return self.documentBaseURI.toRelative(url); 13027 13028 // Convert to absolute 13029 url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); 13030 13031 return url; 13032 }, 13033 13034 addVisual : function(elm) { 13035 var self = this, settings = self.settings, dom = self.dom, cls; 13036 13037 elm = elm || self.getBody(); 13038 13039 if (!is(self.hasVisual)) 13040 self.hasVisual = settings.visual; 13041 13042 each(dom.select('table,a', elm), function(elm) { 13043 var value; 13044 13045 switch (elm.nodeName) { 13046 case 'TABLE': 13047 cls = settings.visual_table_class || 'mceItemTable'; 13048 value = dom.getAttrib(elm, 'border'); 13049 13050 if (!value || value == '0') { 13051 if (self.hasVisual) 13052 dom.addClass(elm, cls); 13053 else 13054 dom.removeClass(elm, cls); 13055 } 13056 13057 return; 13058 13059 case 'A': 13060 if (!dom.getAttrib(elm, 'href', false)) { 13061 value = dom.getAttrib(elm, 'name') || elm.id; 13062 cls = 'mceItemAnchor'; 13063 13064 if (value) { 13065 if (self.hasVisual) 13066 dom.addClass(elm, cls); 13067 else 13068 dom.removeClass(elm, cls); 13069 } 13070 } 13071 13072 return; 13073 } 13074 }); 13075 13076 self.onVisualAid.dispatch(self, elm, self.hasVisual); 13077 }, 13078 13079 remove : function() { 13080 var self = this, elm = self.getContainer(); 13081 13082 if (!self.removed) { 13083 self.removed = 1; // Cancels post remove event execution 13084 self.hide(); 13085 13086 // Don't clear the window or document if content editable 13087 // is enabled since other instances might still be present 13088 if (!self.settings.content_editable) { 13089 Event.unbind(self.getWin()); 13090 Event.unbind(self.getDoc()); 13091 } 13092 13093 Event.unbind(self.getBody()); 13094 Event.clear(elm); 13095 13096 self.execCallback('remove_instance_callback', self); 13097 self.onRemove.dispatch(self); 13098 13099 // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command 13100 self.onExecCommand.listeners = []; 13101 13102 tinymce.remove(self); 13103 DOM.remove(elm); 13104 } 13105 }, 13106 13107 destroy : function(s) { 13108 var t = this; 13109 13110 // One time is enough 13111 if (t.destroyed) 13112 return; 13113 13114 // We must unbind on Gecko since it would otherwise produce the pesky "attempt to run compile-and-go script on a cleared scope" message 13115 if (isGecko) { 13116 Event.unbind(t.getDoc()); 13117 Event.unbind(t.getWin()); 13118 Event.unbind(t.getBody()); 13119 } 13120 13121 if (!s) { 13122 tinymce.removeUnload(t.destroy); 13123 tinyMCE.onBeforeUnload.remove(t._beforeUnload); 13124 13125 // Manual destroy 13126 if (t.theme && t.theme.destroy) 13127 t.theme.destroy(); 13128 13129 // Destroy controls, selection and dom 13130 t.controlManager.destroy(); 13131 t.selection.destroy(); 13132 t.dom.destroy(); 13133 } 13134 13135 if (t.formElement) { 13136 t.formElement.submit = t.formElement._mceOldSubmit; 13137 t.formElement._mceOldSubmit = null; 13138 } 13139 13140 t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null; 13141 13142 if (t.selection) 13143 t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null; 13144 13145 t.destroyed = 1; 13146 }, 13147 13148 // Internal functions 13149 13150 _refreshContentEditable : function() { 13151 var self = this, body, parent; 13152 13153 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again 13154 if (self._isHidden()) { 13155 body = self.getBody(); 13156 parent = body.parentNode; 13157 13158 parent.removeChild(body); 13159 parent.appendChild(body); 13160 13161 body.focus(); 13162 } 13163 }, 13164 13165 _isHidden : function() { 13166 var s; 13167 13168 if (!isGecko) 13169 return 0; 13170 13171 // Weird, wheres that cursor selection? 13172 s = this.selection.getSel(); 13173 return (!s || !s.rangeCount || s.rangeCount === 0); 13174 } 13175 }); 13176 })(tinymce); 13177 (function(tinymce) { 13178 var each = tinymce.each; 13179 13180 tinymce.Editor.prototype.setupEvents = function() { 13181 var self = this, settings = self.settings; 13182 13183 // Add events to the editor 13184 each([ 13185 'onPreInit', 13186 13187 'onBeforeRenderUI', 13188 13189 'onPostRender', 13190 13191 'onLoad', 13192 13193 'onInit', 13194 13195 'onRemove', 13196 13197 'onActivate', 13198 13199 'onDeactivate', 13200 13201 'onClick', 13202 13203 'onEvent', 13204 13205 'onMouseUp', 13206 13207 'onMouseDown', 13208 13209 'onDblClick', 13210 13211 'onKeyDown', 13212 13213 'onKeyUp', 13214 13215 'onKeyPress', 13216 13217 'onContextMenu', 13218 13219 'onSubmit', 13220 13221 'onReset', 13222 13223 'onPaste', 13224 13225 'onPreProcess', 13226 13227 'onPostProcess', 13228 13229 'onBeforeSetContent', 13230 13231 'onBeforeGetContent', 13232 13233 'onSetContent', 13234 13235 'onGetContent', 13236 13237 'onLoadContent', 13238 13239 'onSaveContent', 13240 13241 'onNodeChange', 13242 13243 'onChange', 13244 13245 'onBeforeExecCommand', 13246 13247 'onExecCommand', 13248 13249 'onUndo', 13250 13251 'onRedo', 13252 13253 'onVisualAid', 13254 13255 'onSetProgressState', 13256 13257 'onSetAttrib' 13258 ], function(name) { 13259 self[name] = new tinymce.util.Dispatcher(self); 13260 }); 13261 13262 // Handle legacy cleanup_callback option 13263 if (settings.cleanup_callback) { 13264 self.onBeforeSetContent.add(function(ed, o) { 13265 o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); 13266 }); 13267 13268 self.onPreProcess.add(function(ed, o) { 13269 if (o.set) 13270 ed.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o); 13271 13272 if (o.get) 13273 ed.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o); 13274 }); 13275 13276 self.onPostProcess.add(function(ed, o) { 13277 if (o.set) 13278 o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); 13279 13280 if (o.get) 13281 o.content = ed.execCallback('cleanup_callback', 'get_from_editor', o.content, o); 13282 }); 13283 } 13284 13285 // Handle legacy save_callback option 13286 if (settings.save_callback) { 13287 self.onGetContent.add(function(ed, o) { 13288 if (o.save) 13289 o.content = ed.execCallback('save_callback', ed.id, o.content, ed.getBody()); 13290 }); 13291 } 13292 13293 // Handle legacy handle_event_callback option 13294 if (settings.handle_event_callback) { 13295 self.onEvent.add(function(ed, e, o) { 13296 if (self.execCallback('handle_event_callback', e, ed, o) === false) { 13297 e.preventDefault(); 13298 e.stopPropagation(); 13299 } 13300 }); 13301 } 13302 13303 // Handle legacy handle_node_change_callback option 13304 if (settings.handle_node_change_callback) { 13305 self.onNodeChange.add(function(ed, cm, n) { 13306 ed.execCallback('handle_node_change_callback', ed.id, n, -1, -1, true, ed.selection.isCollapsed()); 13307 }); 13308 } 13309 13310 // Handle legacy save_callback option 13311 if (settings.save_callback) { 13312 self.onSaveContent.add(function(ed, o) { 13313 var h = ed.execCallback('save_callback', ed.id, o.content, ed.getBody()); 13314 13315 if (h) 13316 o.content = h; 13317 }); 13318 } 13319 13320 // Handle legacy onchange_callback option 13321 if (settings.onchange_callback) { 13322 self.onChange.add(function(ed, l) { 13323 ed.execCallback('onchange_callback', ed, l); 13324 }); 13325 } 13326 }; 13327 13328 tinymce.Editor.prototype.bindNativeEvents = function() { 13329 // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset 13330 var self = this, i, settings = self.settings, dom = self.dom, nativeToDispatcherMap; 13331 13332 nativeToDispatcherMap = { 13333 mouseup : 'onMouseUp', 13334 mousedown : 'onMouseDown', 13335 click : 'onClick', 13336 keyup : 'onKeyUp', 13337 keydown : 'onKeyDown', 13338 keypress : 'onKeyPress', 13339 submit : 'onSubmit', 13340 reset : 'onReset', 13341 contextmenu : 'onContextMenu', 13342 dblclick : 'onDblClick', 13343 paste : 'onPaste' // Doesn't work in all browsers yet 13344 }; 13345 13346 // Handler that takes a native event and sends it out to a dispatcher like onKeyDown 13347 function eventHandler(evt, args) { 13348 var type = evt.type; 13349 13350 // Don't fire events when it's removed 13351 if (self.removed) 13352 return; 13353 13354 // Sends the native event out to a global dispatcher then to the specific event dispatcher 13355 if (self.onEvent.dispatch(self, evt, args) !== false) { 13356 self[nativeToDispatcherMap[evt.fakeType || evt.type]].dispatch(self, evt, args); 13357 } 13358 }; 13359 13360 // Opera doesn't support focus event for contentEditable elements so we need to fake it 13361 function doOperaFocus(e) { 13362 self.focus(true); 13363 }; 13364 13365 function nodeChanged(ed, e) { 13366 // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything 13367 if (e.keyCode != 65 || !tinymce.VK.metaKeyPressed(e)) { 13368 self.selection.normalize(); 13369 } 13370 13371 self.nodeChanged(); 13372 } 13373 13374 // Add DOM events 13375 each(nativeToDispatcherMap, function(dispatcherName, nativeName) { 13376 var root = settings.content_editable ? self.getBody() : self.getDoc(); 13377 13378 switch (nativeName) { 13379 case 'contextmenu': 13380 dom.bind(root, nativeName, eventHandler); 13381 break; 13382 13383 case 'paste': 13384 dom.bind(self.getBody(), nativeName, eventHandler); 13385 break; 13386 13387 case 'submit': 13388 case 'reset': 13389 dom.bind(self.getElement().form || tinymce.DOM.getParent(self.id, 'form'), nativeName, eventHandler); 13390 break; 13391 13392 default: 13393 dom.bind(root, nativeName, eventHandler); 13394 } 13395 }); 13396 13397 // Set the editor as active when focused 13398 dom.bind(settings.content_editable ? self.getBody() : (tinymce.isGecko ? self.getDoc() : self.getWin()), 'focus', function(e) { 13399 self.focus(true); 13400 }); 13401 13402 if (settings.content_editable && tinymce.isOpera) { 13403 dom.bind(self.getBody(), 'click', doOperaFocus); 13404 dom.bind(self.getBody(), 'keydown', doOperaFocus); 13405 } 13406 13407 // Add node change handler 13408 self.onMouseUp.add(nodeChanged); 13409 13410 self.onKeyUp.add(function(ed, e) { 13411 var keyCode = e.keyCode; 13412 13413 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || keyCode == 46 || keyCode == 8 || (tinymce.isMac && (keyCode == 91 || keyCode == 93)) || e.ctrlKey) 13414 nodeChanged(ed, e); 13415 }); 13416 13417 // Add reset handler 13418 self.onReset.add(function() { 13419 self.setContent(self.startContent, {format : 'raw'}); 13420 }); 13421 13422 // Add shortcuts 13423 function handleShortcut(e, execute) { 13424 if (e.altKey || e.ctrlKey || e.metaKey) { 13425 each(self.shortcuts, function(shortcut) { 13426 var ctrlState = tinymce.isMac ? e.metaKey : e.ctrlKey; 13427 13428 if (shortcut.ctrl != ctrlState || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) 13429 return; 13430 13431 if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { 13432 e.preventDefault(); 13433 13434 if (execute) { 13435 shortcut.func.call(shortcut.scope); 13436 } 13437 13438 return true; 13439 } 13440 }); 13441 } 13442 }; 13443 13444 self.onKeyUp.add(function(ed, e) { 13445 handleShortcut(e); 13446 }); 13447 13448 self.onKeyPress.add(function(ed, e) { 13449 handleShortcut(e); 13450 }); 13451 13452 self.onKeyDown.add(function(ed, e) { 13453 handleShortcut(e, true); 13454 }); 13455 13456 if (tinymce.isOpera) { 13457 self.onClick.add(function(ed, e) { 13458 e.preventDefault(); 13459 }); 13460 } 13461 }; 13462 })(tinymce); 13463 (function(tinymce) { 13464 // Added for compression purposes 13465 var each = tinymce.each, undef, TRUE = true, FALSE = false; 13466 13467 tinymce.EditorCommands = function(editor) { 13468 var dom = editor.dom, 13469 selection = editor.selection, 13470 commands = {state: {}, exec : {}, value : {}}, 13471 settings = editor.settings, 13472 formatter = editor.formatter, 13473 bookmark; 13474 13475 function execCommand(command, ui, value) { 13476 var func; 13477 13478 command = command.toLowerCase(); 13479 if (func = commands.exec[command]) { 13480 func(command, ui, value); 13481 return TRUE; 13482 } 13483 13484 return FALSE; 13485 }; 13486 13487 function queryCommandState(command) { 13488 var func; 13489 13490 command = command.toLowerCase(); 13491 if (func = commands.state[command]) 13492 return func(command); 13493 13494 return -1; 13495 }; 13496 13497 function queryCommandValue(command) { 13498 var func; 13499 13500 command = command.toLowerCase(); 13501 if (func = commands.value[command]) 13502 return func(command); 13503 13504 return FALSE; 13505 }; 13506 13507 function addCommands(command_list, type) { 13508 type = type || 'exec'; 13509 13510 each(command_list, function(callback, command) { 13511 each(command.toLowerCase().split(','), function(command) { 13512 commands[type][command] = callback; 13513 }); 13514 }); 13515 }; 13516 13517 // Expose public methods 13518 tinymce.extend(this, { 13519 execCommand : execCommand, 13520 queryCommandState : queryCommandState, 13521 queryCommandValue : queryCommandValue, 13522 addCommands : addCommands 13523 }); 13524 13525 // Private methods 13526 13527 function execNativeCommand(command, ui, value) { 13528 if (ui === undef) 13529 ui = FALSE; 13530 13531 if (value === undef) 13532 value = null; 13533 13534 return editor.getDoc().execCommand(command, ui, value); 13535 }; 13536 13537 function isFormatMatch(name) { 13538 return formatter.match(name); 13539 }; 13540 13541 function toggleFormat(name, value) { 13542 formatter.toggle(name, value ? {value : value} : undef); 13543 }; 13544 13545 function storeSelection(type) { 13546 bookmark = selection.getBookmark(type); 13547 }; 13548 13549 function restoreSelection() { 13550 selection.moveToBookmark(bookmark); 13551 }; 13552 13553 // Add execCommand overrides 13554 addCommands({ 13555 // Ignore these, added for compatibility 13556 'mceResetDesignMode,mceBeginUndoLevel' : function() {}, 13557 13558 // Add undo manager logic 13559 'mceEndUndoLevel,mceAddUndoLevel' : function() { 13560 editor.undoManager.add(); 13561 }, 13562 13563 'Cut,Copy,Paste' : function(command) { 13564 var doc = editor.getDoc(), failed; 13565 13566 // Try executing the native command 13567 try { 13568 execNativeCommand(command); 13569 } catch (ex) { 13570 // Command failed 13571 failed = TRUE; 13572 } 13573 13574 // Present alert message about clipboard access not being available 13575 if (failed || !doc.queryCommandSupported(command)) { 13576 if (tinymce.isGecko) { 13577 editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) { 13578 if (state) 13579 open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank'); 13580 }); 13581 } else 13582 editor.windowManager.alert(editor.getLang('clipboard_no_support')); 13583 } 13584 }, 13585 13586 // Override unlink command 13587 unlink : function(command) { 13588 if (selection.isCollapsed()) 13589 selection.select(selection.getNode()); 13590 13591 execNativeCommand(command); 13592 selection.collapse(FALSE); 13593 }, 13594 13595 // Override justify commands to use the text formatter engine 13596 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { 13597 var align = command.substring(7); 13598 13599 // Remove all other alignments first 13600 each('left,center,right,full'.split(','), function(name) { 13601 if (align != name) 13602 formatter.remove('align' + name); 13603 }); 13604 13605 toggleFormat('align' + align); 13606 execCommand('mceRepaint'); 13607 }, 13608 13609 // Override list commands to fix WebKit bug 13610 'InsertUnorderedList,InsertOrderedList' : function(command) { 13611 var listElm, listParent; 13612 13613 execNativeCommand(command); 13614 13615 // WebKit produces lists within block elements so we need to split them 13616 // we will replace the native list creation logic to custom logic later on 13617 // TODO: Remove this when the list creation logic is removed 13618 listElm = dom.getParent(selection.getNode(), 'ol,ul'); 13619 if (listElm) { 13620 listParent = listElm.parentNode; 13621 13622 // If list is within a text block then split that block 13623 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { 13624 storeSelection(); 13625 dom.split(listParent, listElm); 13626 restoreSelection(); 13627 } 13628 } 13629 }, 13630 13631 // Override commands to use the text formatter engine 13632 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { 13633 toggleFormat(command); 13634 }, 13635 13636 // Override commands to use the text formatter engine 13637 'ForeColor,HiliteColor,FontName' : function(command, ui, value) { 13638 toggleFormat(command, value); 13639 }, 13640 13641 FontSize : function(command, ui, value) { 13642 var fontClasses, fontSizes; 13643 13644 // Convert font size 1-7 to styles 13645 if (value >= 1 && value <= 7) { 13646 fontSizes = tinymce.explode(settings.font_size_style_values); 13647 fontClasses = tinymce.explode(settings.font_size_classes); 13648 13649 if (fontClasses) 13650 value = fontClasses[value - 1] || value; 13651 else 13652 value = fontSizes[value - 1] || value; 13653 } 13654 13655 toggleFormat(command, value); 13656 }, 13657 13658 RemoveFormat : function(command) { 13659 formatter.remove(command); 13660 }, 13661 13662 mceBlockQuote : function(command) { 13663 toggleFormat('blockquote'); 13664 }, 13665 13666 FormatBlock : function(command, ui, value) { 13667 return toggleFormat(value || 'p'); 13668 }, 13669 13670 mceCleanup : function() { 13671 var bookmark = selection.getBookmark(); 13672 13673 editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE}); 13674 13675 selection.moveToBookmark(bookmark); 13676 }, 13677 13678 mceRemoveNode : function(command, ui, value) { 13679 var node = value || selection.getNode(); 13680 13681 // Make sure that the body node isn't removed 13682 if (node != editor.getBody()) { 13683 storeSelection(); 13684 editor.dom.remove(node, TRUE); 13685 restoreSelection(); 13686 } 13687 }, 13688 13689 mceSelectNodeDepth : function(command, ui, value) { 13690 var counter = 0; 13691 13692 dom.getParent(selection.getNode(), function(node) { 13693 if (node.nodeType == 1 && counter++ == value) { 13694 selection.select(node); 13695 return FALSE; 13696 } 13697 }, editor.getBody()); 13698 }, 13699 13700 mceSelectNode : function(command, ui, value) { 13701 selection.select(value); 13702 }, 13703 13704 mceInsertContent : function(command, ui, value) { 13705 var parser, serializer, parentNode, rootNode, fragment, args, 13706 marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement; 13707 13708 //selection.normalize(); 13709 13710 // Setup parser and serializer 13711 parser = editor.parser; 13712 serializer = new tinymce.html.Serializer({}, editor.schema); 13713 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>'; 13714 13715 // Run beforeSetContent handlers on the HTML to be inserted 13716 args = {content: value, format: 'html'}; 13717 selection.onBeforeSetContent.dispatch(selection, args); 13718 value = args.content; 13719 13720 // Add caret at end of contents if it's missing 13721 if (value.indexOf('{$caret}') == -1) 13722 value += '{$caret}'; 13723 13724 // Replace the caret marker with a span bookmark element 13725 value = value.replace(/\{\$caret\}/, bookmarkHtml); 13726 13727 // Insert node maker where we will insert the new HTML and get it's parent 13728 if (!selection.isCollapsed()) 13729 editor.getDoc().execCommand('Delete', false, null); 13730 13731 parentNode = selection.getNode(); 13732 13733 // Parse the fragment within the context of the parent node 13734 args = {context : parentNode.nodeName.toLowerCase()}; 13735 fragment = parser.parse(value, args); 13736 13737 // Move the caret to a more suitable location 13738 node = fragment.lastChild; 13739 if (node.attr('id') == 'mce_marker') { 13740 marker = node; 13741 13742 for (node = node.prev; node; node = node.walk(true)) { 13743 if (node.type == 3 || !dom.isBlock(node.name)) { 13744 node.parent.insert(marker, node, node.name === 'br'); 13745 break; 13746 } 13747 } 13748 } 13749 13750 // If parser says valid we can insert the contents into that parent 13751 if (!args.invalid) { 13752 value = serializer.serialize(fragment); 13753 13754 // Check if parent is empty or only has one BR element then set the innerHTML of that parent 13755 node = parentNode.firstChild; 13756 node2 = parentNode.lastChild; 13757 if (!node || (node === node2 && node.nodeName === 'BR')) 13758 dom.setHTML(parentNode, value); 13759 else 13760 selection.setContent(value); 13761 } else { 13762 // If the fragment was invalid within that context then we need 13763 // to parse and process the parent it's inserted into 13764 13765 // Insert bookmark node and get the parent 13766 selection.setContent(bookmarkHtml); 13767 parentNode = editor.selection.getNode(); 13768 rootNode = editor.getBody(); 13769 13770 // Opera will return the document node when selection is in root 13771 if (parentNode.nodeType == 9) 13772 parentNode = node = rootNode; 13773 else 13774 node = parentNode; 13775 13776 // Find the ancestor just before the root element 13777 while (node !== rootNode) { 13778 parentNode = node; 13779 node = node.parentNode; 13780 } 13781 13782 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that 13783 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); 13784 value = serializer.serialize( 13785 parser.parse( 13786 // Need to replace by using a function since $ in the contents would otherwise be a problem 13787 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() { 13788 return serializer.serialize(fragment); 13789 }) 13790 ) 13791 ); 13792 13793 // Set the inner/outer HTML depending on if we are in the root or not 13794 if (parentNode == rootNode) 13795 dom.setHTML(rootNode, value); 13796 else 13797 dom.setOuterHTML(parentNode, value); 13798 } 13799 13800 marker = dom.get('mce_marker'); 13801 13802 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well 13803 nodeRect = dom.getRect(marker); 13804 viewPortRect = dom.getViewPort(editor.getWin()); 13805 13806 // Check if node is out side the viewport if it is then scroll to it 13807 if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) || 13808 (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) { 13809 viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody(); 13810 viewportBodyElement.scrollLeft = nodeRect.x; 13811 viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25; 13812 } 13813 13814 // Move selection before marker and remove it 13815 rng = dom.createRng(); 13816 13817 // If previous sibling is a text node set the selection to the end of that node 13818 node = marker.previousSibling; 13819 if (node && node.nodeType == 3) { 13820 rng.setStart(node, node.nodeValue.length); 13821 } else { 13822 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node 13823 rng.setStartBefore(marker); 13824 rng.setEndBefore(marker); 13825 } 13826 13827 // Remove the marker node and set the new range 13828 dom.remove(marker); 13829 selection.setRng(rng); 13830 13831 // Dispatch after event and add any visual elements needed 13832 selection.onSetContent.dispatch(selection, args); 13833 editor.addVisual(); 13834 }, 13835 13836 mceInsertRawHTML : function(command, ui, value) { 13837 selection.setContent('tiny_mce_marker'); 13838 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value })); 13839 }, 13840 13841 mceToggleFormat : function(command, ui, value) { 13842 toggleFormat(value); 13843 }, 13844 13845 mceSetContent : function(command, ui, value) { 13846 editor.setContent(value); 13847 }, 13848 13849 'Indent,Outdent' : function(command) { 13850 var intentValue, indentUnit, value; 13851 13852 // Setup indent level 13853 intentValue = settings.indentation; 13854 indentUnit = /[a-z%]+$/i.exec(intentValue); 13855 intentValue = parseInt(intentValue); 13856 13857 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { 13858 // If forced_root_blocks is set to false we don't have a block to indent so lets create a div 13859 if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { 13860 formatter.apply('div'); 13861 } 13862 13863 each(selection.getSelectedBlocks(), function(element) { 13864 if (command == 'outdent') { 13865 value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue); 13866 dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : ''); 13867 } else 13868 dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit); 13869 }); 13870 } else 13871 execNativeCommand(command); 13872 }, 13873 13874 mceRepaint : function() { 13875 var bookmark; 13876 13877 if (tinymce.isGecko) { 13878 try { 13879 storeSelection(TRUE); 13880 13881 if (selection.getSel()) 13882 selection.getSel().selectAllChildren(editor.getBody()); 13883 13884 selection.collapse(TRUE); 13885 restoreSelection(); 13886 } catch (ex) { 13887 // Ignore 13888 } 13889 } 13890 }, 13891 13892 mceToggleFormat : function(command, ui, value) { 13893 formatter.toggle(value); 13894 }, 13895 13896 InsertHorizontalRule : function() { 13897 editor.execCommand('mceInsertContent', false, '<hr />'); 13898 }, 13899 13900 mceToggleVisualAid : function() { 13901 editor.hasVisual = !editor.hasVisual; 13902 editor.addVisual(); 13903 }, 13904 13905 mceReplaceContent : function(command, ui, value) { 13906 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); 13907 }, 13908 13909 mceInsertLink : function(command, ui, value) { 13910 var anchor; 13911 13912 if (typeof(value) == 'string') 13913 value = {href : value}; 13914 13915 anchor = dom.getParent(selection.getNode(), 'a'); 13916 13917 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. 13918 value.href = value.href.replace(' ', '%20'); 13919 13920 // Remove existing links if there could be child links or that the href isn't specified 13921 if (!anchor || !value.href) { 13922 formatter.remove('link'); 13923 } 13924 13925 // Apply new link to selection 13926 if (value.href) { 13927 formatter.apply('link', value, anchor); 13928 } 13929 }, 13930 13931 selectAll : function() { 13932 var root = dom.getRoot(), rng = dom.createRng(); 13933 13934 rng.setStart(root, 0); 13935 rng.setEnd(root, root.childNodes.length); 13936 13937 editor.selection.setRng(rng); 13938 } 13939 }); 13940 13941 // Add queryCommandState overrides 13942 addCommands({ 13943 // Override justify commands 13944 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { 13945 var name = 'align' + command.substring(7); 13946 var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); 13947 var matches = tinymce.map(nodes, function(node) { 13948 return !!formatter.matchNode(node, name); 13949 }); 13950 return tinymce.inArray(matches, TRUE) !== -1; 13951 }, 13952 13953 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { 13954 return isFormatMatch(command); 13955 }, 13956 13957 mceBlockQuote : function() { 13958 return isFormatMatch('blockquote'); 13959 }, 13960 13961 Outdent : function() { 13962 var node; 13963 13964 if (settings.inline_styles) { 13965 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) 13966 return TRUE; 13967 13968 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) 13969 return TRUE; 13970 } 13971 13972 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')); 13973 }, 13974 13975 'InsertUnorderedList,InsertOrderedList' : function(command) { 13976 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL'); 13977 } 13978 }, 'state'); 13979 13980 // Add queryCommandValue overrides 13981 addCommands({ 13982 'FontSize,FontName' : function(command) { 13983 var value = 0, parent; 13984 13985 if (parent = dom.getParent(selection.getNode(), 'span')) { 13986 if (command == 'fontsize') 13987 value = parent.style.fontSize; 13988 else 13989 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); 13990 } 13991 13992 return value; 13993 } 13994 }, 'value'); 13995 13996 // Add undo manager logic 13997 addCommands({ 13998 Undo : function() { 13999 editor.undoManager.undo(); 14000 }, 14001 14002 Redo : function() { 14003 editor.undoManager.redo(); 14004 } 14005 }); 14006 }; 14007 })(tinymce); 14008 14009 (function(tinymce) { 14010 var Dispatcher = tinymce.util.Dispatcher; 14011 14012 tinymce.UndoManager = function(editor) { 14013 var self, index = 0, data = [], beforeBookmark, onAdd, onUndo, onRedo; 14014 14015 function getContent() { 14016 // Remove whitespace before/after and remove pure bogus nodes 14017 return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}).replace(/<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>/g, '')); 14018 }; 14019 14020 function addNonTypingUndoLevel() { 14021 self.typing = false; 14022 self.add(); 14023 }; 14024 14025 // Create event instances 14026 onBeforeAdd = new Dispatcher(self); 14027 onAdd = new Dispatcher(self); 14028 onUndo = new Dispatcher(self); 14029 onRedo = new Dispatcher(self); 14030 14031 // Pass though onAdd event from UndoManager to Editor as onChange 14032 onAdd.add(function(undoman, level) { 14033 if (undoman.hasUndo()) 14034 return editor.onChange.dispatch(editor, level, undoman); 14035 }); 14036 14037 // Pass though onUndo event from UndoManager to Editor 14038 onUndo.add(function(undoman, level) { 14039 return editor.onUndo.dispatch(editor, level, undoman); 14040 }); 14041 14042 // Pass though onRedo event from UndoManager to Editor 14043 onRedo.add(function(undoman, level) { 14044 return editor.onRedo.dispatch(editor, level, undoman); 14045 }); 14046 14047 // Add initial undo level when the editor is initialized 14048 editor.onInit.add(function() { 14049 self.add(); 14050 }); 14051 14052 // Get position before an execCommand is processed 14053 editor.onBeforeExecCommand.add(function(ed, cmd, ui, val, args) { 14054 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) { 14055 self.beforeChange(); 14056 } 14057 }); 14058 14059 // Add undo level after an execCommand call was made 14060 editor.onExecCommand.add(function(ed, cmd, ui, val, args) { 14061 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) { 14062 self.add(); 14063 } 14064 }); 14065 14066 // Add undo level on save contents, drag end and blur/focusout 14067 editor.onSaveContent.add(addNonTypingUndoLevel); 14068 editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel); 14069 editor.dom.bind(editor.getDoc(), tinymce.isGecko ? 'blur' : 'focusout', function(e) { 14070 if (!editor.removed && self.typing) { 14071 addNonTypingUndoLevel(); 14072 } 14073 }); 14074 14075 editor.onKeyUp.add(function(editor, e) { 14076 var keyCode = e.keyCode; 14077 14078 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { 14079 addNonTypingUndoLevel(); 14080 } 14081 }); 14082 14083 editor.onKeyDown.add(function(editor, e) { 14084 var keyCode = e.keyCode; 14085 14086 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter 14087 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) { 14088 if (self.typing) { 14089 addNonTypingUndoLevel(); 14090 } 14091 14092 return; 14093 } 14094 14095 // If key isn't shift,ctrl,alt,capslock,metakey 14096 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) { 14097 self.beforeChange(); 14098 self.typing = true; 14099 self.add(); 14100 } 14101 }); 14102 14103 editor.onMouseDown.add(function(editor, e) { 14104 if (self.typing) { 14105 addNonTypingUndoLevel(); 14106 } 14107 }); 14108 14109 // Add keyboard shortcuts for undo/redo keys 14110 editor.addShortcut('ctrl+z', 'undo_desc', 'Undo'); 14111 editor.addShortcut('ctrl+y', 'redo_desc', 'Redo'); 14112 14113 self = { 14114 // Explose for debugging reasons 14115 data : data, 14116 14117 typing : false, 14118 14119 onBeforeAdd: onBeforeAdd, 14120 14121 onAdd : onAdd, 14122 14123 onUndo : onUndo, 14124 14125 onRedo : onRedo, 14126 14127 beforeChange : function() { 14128 beforeBookmark = editor.selection.getBookmark(2, true); 14129 }, 14130 14131 add : function(level) { 14132 var i, settings = editor.settings, lastLevel; 14133 14134 level = level || {}; 14135 level.content = getContent(); 14136 14137 self.onBeforeAdd.dispatch(self, level); 14138 14139 // Add undo level if needed 14140 lastLevel = data[index]; 14141 if (lastLevel && lastLevel.content == level.content) 14142 return null; 14143 14144 // Set before bookmark on previous level 14145 if (data[index]) 14146 data[index].beforeBookmark = beforeBookmark; 14147 14148 // Time to compress 14149 if (settings.custom_undo_redo_levels) { 14150 if (data.length > settings.custom_undo_redo_levels) { 14151 for (i = 0; i < data.length - 1; i++) 14152 data[i] = data[i + 1]; 14153 14154 data.length--; 14155 index = data.length; 14156 } 14157 } 14158 14159 // Get a non intrusive normalized bookmark 14160 level.bookmark = editor.selection.getBookmark(2, true); 14161 14162 // Crop array if needed 14163 if (index < data.length - 1) 14164 data.length = index + 1; 14165 14166 data.push(level); 14167 index = data.length - 1; 14168 14169 self.onAdd.dispatch(self, level); 14170 editor.isNotDirty = 0; 14171 14172 return level; 14173 }, 14174 14175 undo : function() { 14176 var level, i; 14177 14178 if (self.typing) { 14179 self.add(); 14180 self.typing = false; 14181 } 14182 14183 if (index > 0) { 14184 level = data[--index]; 14185 14186 editor.setContent(level.content, {format : 'raw'}); 14187 editor.selection.moveToBookmark(level.beforeBookmark); 14188 14189 self.onUndo.dispatch(self, level); 14190 } 14191 14192 return level; 14193 }, 14194 14195 redo : function() { 14196 var level; 14197 14198 if (index < data.length - 1) { 14199 level = data[++index]; 14200 14201 editor.setContent(level.content, {format : 'raw'}); 14202 editor.selection.moveToBookmark(level.bookmark); 14203 14204 self.onRedo.dispatch(self, level); 14205 } 14206 14207 return level; 14208 }, 14209 14210 clear : function() { 14211 data = []; 14212 index = 0; 14213 self.typing = false; 14214 }, 14215 14216 hasUndo : function() { 14217 return index > 0 || this.typing; 14218 }, 14219 14220 hasRedo : function() { 14221 return index < data.length - 1 && !this.typing; 14222 } 14223 }; 14224 14225 return self; 14226 }; 14227 })(tinymce); 14228 14229 tinymce.ForceBlocks = function(editor) { 14230 var settings = editor.settings, dom = editor.dom, selection = editor.selection, blockElements = editor.schema.getBlockElements(); 14231 14232 function addRootBlocks() { 14233 var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped, isInEditorDocument; 14234 14235 if (!node || node.nodeType !== 1 || !settings.forced_root_block) 14236 return; 14237 14238 // Check if node is wrapped in block 14239 while (node && node != rootNode) { 14240 if (blockElements[node.nodeName]) 14241 return; 14242 14243 node = node.parentNode; 14244 } 14245 14246 // Get current selection 14247 rng = selection.getRng(); 14248 if (rng.setStart) { 14249 startContainer = rng.startContainer; 14250 startOffset = rng.startOffset; 14251 endContainer = rng.endContainer; 14252 endOffset = rng.endOffset; 14253 } else { 14254 // Force control range into text range 14255 if (rng.item) { 14256 node = rng.item(0); 14257 rng = editor.getDoc().body.createTextRange(); 14258 rng.moveToElementText(node); 14259 } 14260 14261 isInEditorDocument = rng.parentElement().ownerDocument === editor.getDoc(); 14262 tmpRng = rng.duplicate(); 14263 tmpRng.collapse(true); 14264 startOffset = tmpRng.move('character', offset) * -1; 14265 14266 if (!tmpRng.collapsed) { 14267 tmpRng = rng.duplicate(); 14268 tmpRng.collapse(false); 14269 endOffset = (tmpRng.move('character', offset) * -1) - startOffset; 14270 } 14271 } 14272 14273 // Wrap non block elements and text nodes 14274 node = rootNode.firstChild; 14275 while (node) { 14276 if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { 14277 if (!rootBlockNode) { 14278 rootBlockNode = dom.create(settings.forced_root_block); 14279 node.parentNode.insertBefore(rootBlockNode, node); 14280 wrapped = true; 14281 } 14282 14283 tempNode = node; 14284 node = node.nextSibling; 14285 rootBlockNode.appendChild(tempNode); 14286 } else { 14287 rootBlockNode = null; 14288 node = node.nextSibling; 14289 } 14290 } 14291 14292 if (wrapped) { 14293 if (rng.setStart) { 14294 rng.setStart(startContainer, startOffset); 14295 rng.setEnd(endContainer, endOffset); 14296 selection.setRng(rng); 14297 } else { 14298 // Only select if the previous selection was inside the document to prevent auto focus in quirks mode 14299 if (isInEditorDocument) { 14300 try { 14301 rng = editor.getDoc().body.createTextRange(); 14302 rng.moveToElementText(rootNode); 14303 rng.collapse(true); 14304 rng.moveStart('character', startOffset); 14305 14306 if (endOffset > 0) 14307 rng.moveEnd('character', endOffset); 14308 14309 rng.select(); 14310 } catch (ex) { 14311 // Ignore 14312 } 14313 } 14314 } 14315 14316 editor.nodeChanged(); 14317 } 14318 }; 14319 14320 // Force root blocks 14321 if (settings.forced_root_block) { 14322 editor.onKeyUp.add(addRootBlocks); 14323 editor.onNodeChange.add(addRootBlocks); 14324 } 14325 }; 14326 14327 (function(tinymce) { 14328 // Shorten names 14329 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend; 14330 14331 tinymce.create('tinymce.ControlManager', { 14332 ControlManager : function(ed, s) { 14333 var t = this, i; 14334 14335 s = s || {}; 14336 t.editor = ed; 14337 t.controls = {}; 14338 t.onAdd = new tinymce.util.Dispatcher(t); 14339 t.onPostRender = new tinymce.util.Dispatcher(t); 14340 t.prefix = s.prefix || ed.id + '_'; 14341 t._cls = {}; 14342 14343 t.onPostRender.add(function() { 14344 each(t.controls, function(c) { 14345 c.postRender(); 14346 }); 14347 }); 14348 }, 14349 14350 get : function(id) { 14351 return this.controls[this.prefix + id] || this.controls[id]; 14352 }, 14353 14354 setActive : function(id, s) { 14355 var c = null; 14356 14357 if (c = this.get(id)) 14358 c.setActive(s); 14359 14360 return c; 14361 }, 14362 14363 setDisabled : function(id, s) { 14364 var c = null; 14365 14366 if (c = this.get(id)) 14367 c.setDisabled(s); 14368 14369 return c; 14370 }, 14371 14372 add : function(c) { 14373 var t = this; 14374 14375 if (c) { 14376 t.controls[c.id] = c; 14377 t.onAdd.dispatch(c, t); 14378 } 14379 14380 return c; 14381 }, 14382 14383 createControl : function(name) { 14384 var ctrl, i, l, self = this, editor = self.editor, factories, ctrlName; 14385 14386 // Build control factory cache 14387 if (!self.controlFactories) { 14388 self.controlFactories = []; 14389 each(editor.plugins, function(plugin) { 14390 if (plugin.createControl) { 14391 self.controlFactories.push(plugin); 14392 } 14393 }); 14394 } 14395 14396 // Create controls by asking cached factories 14397 factories = self.controlFactories; 14398 for (i = 0, l = factories.length; i < l; i++) { 14399 ctrl = factories[i].createControl(name, self); 14400 14401 if (ctrl) { 14402 return self.add(ctrl); 14403 } 14404 } 14405 14406 // Create sepearator 14407 if (name === "|" || name === "separator") { 14408 return self.createSeparator(); 14409 } 14410 14411 // Create control from button collection 14412 if (editor.buttons && (ctrl = editor.buttons[name])) { 14413 return self.createButton(name, ctrl); 14414 } 14415 14416 return self.add(ctrl); 14417 }, 14418 14419 createDropMenu : function(id, s, cc) { 14420 var t = this, ed = t.editor, c, bm, v, cls; 14421 14422 s = extend({ 14423 'class' : 'mceDropDown', 14424 constrain : ed.settings.constrain_menus 14425 }, s); 14426 14427 s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin'; 14428 if (v = ed.getParam('skin_variant')) 14429 s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1); 14430 14431 s['class'] += ed.settings.directionality == "rtl" ? ' mceRtl' : ''; 14432 14433 id = t.prefix + id; 14434 cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu; 14435 c = t.controls[id] = new cls(id, s); 14436 c.onAddItem.add(function(c, o) { 14437 var s = o.settings; 14438 14439 s.title = ed.getLang(s.title, s.title); 14440 14441 if (!s.onclick) { 14442 s.onclick = function(v) { 14443 if (s.cmd) 14444 ed.execCommand(s.cmd, s.ui || false, s.value); 14445 }; 14446 } 14447 }); 14448 14449 ed.onRemove.add(function() { 14450 c.destroy(); 14451 }); 14452 14453 // Fix for bug #1897785, #1898007 14454 if (tinymce.isIE) { 14455 c.onShowMenu.add(function() { 14456 // IE 8 needs focus in order to store away a range with the current collapsed caret location 14457 ed.focus(); 14458 14459 bm = ed.selection.getBookmark(1); 14460 }); 14461 14462 c.onHideMenu.add(function() { 14463 if (bm) { 14464 ed.selection.moveToBookmark(bm); 14465 bm = 0; 14466 } 14467 }); 14468 } 14469 14470 return t.add(c); 14471 }, 14472 14473 createListBox : function(id, s, cc) { 14474 var t = this, ed = t.editor, cmd, c, cls; 14475 14476 if (t.get(id)) 14477 return null; 14478 14479 s.title = ed.translate(s.title); 14480 s.scope = s.scope || ed; 14481 14482 if (!s.onselect) { 14483 s.onselect = function(v) { 14484 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14485 }; 14486 } 14487 14488 s = extend({ 14489 title : s.title, 14490 'class' : 'mce_' + id, 14491 scope : s.scope, 14492 control_manager : t 14493 }, s); 14494 14495 id = t.prefix + id; 14496 14497 14498 function useNativeListForAccessibility(ed) { 14499 return ed.settings.use_accessible_selects && !tinymce.isGecko 14500 } 14501 14502 if (ed.settings.use_native_selects || useNativeListForAccessibility(ed)) 14503 c = new tinymce.ui.NativeListBox(id, s); 14504 else { 14505 cls = cc || t._cls.listbox || tinymce.ui.ListBox; 14506 c = new cls(id, s, ed); 14507 } 14508 14509 t.controls[id] = c; 14510 14511 // Fix focus problem in Safari 14512 if (tinymce.isWebKit) { 14513 c.onPostRender.add(function(c, n) { 14514 // Store bookmark on mousedown 14515 Event.add(n, 'mousedown', function() { 14516 ed.bookmark = ed.selection.getBookmark(1); 14517 }); 14518 14519 // Restore on focus, since it might be lost 14520 Event.add(n, 'focus', function() { 14521 ed.selection.moveToBookmark(ed.bookmark); 14522 ed.bookmark = null; 14523 }); 14524 }); 14525 } 14526 14527 if (c.hideMenu) 14528 ed.onMouseDown.add(c.hideMenu, c); 14529 14530 return t.add(c); 14531 }, 14532 14533 createButton : function(id, s, cc) { 14534 var t = this, ed = t.editor, o, c, cls; 14535 14536 if (t.get(id)) 14537 return null; 14538 14539 s.title = ed.translate(s.title); 14540 s.label = ed.translate(s.label); 14541 s.scope = s.scope || ed; 14542 14543 if (!s.onclick && !s.menu_button) { 14544 s.onclick = function() { 14545 ed.execCommand(s.cmd, s.ui || false, s.value); 14546 }; 14547 } 14548 14549 s = extend({ 14550 title : s.title, 14551 'class' : 'mce_' + id, 14552 unavailable_prefix : ed.getLang('unavailable', ''), 14553 scope : s.scope, 14554 control_manager : t 14555 }, s); 14556 14557 id = t.prefix + id; 14558 14559 if (s.menu_button) { 14560 cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; 14561 c = new cls(id, s, ed); 14562 ed.onMouseDown.add(c.hideMenu, c); 14563 } else { 14564 cls = t._cls.button || tinymce.ui.Button; 14565 c = new cls(id, s, ed); 14566 } 14567 14568 return t.add(c); 14569 }, 14570 14571 createMenuButton : function(id, s, cc) { 14572 s = s || {}; 14573 s.menu_button = 1; 14574 14575 return this.createButton(id, s, cc); 14576 }, 14577 14578 createSplitButton : function(id, s, cc) { 14579 var t = this, ed = t.editor, cmd, c, cls; 14580 14581 if (t.get(id)) 14582 return null; 14583 14584 s.title = ed.translate(s.title); 14585 s.scope = s.scope || ed; 14586 14587 if (!s.onclick) { 14588 s.onclick = function(v) { 14589 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14590 }; 14591 } 14592 14593 if (!s.onselect) { 14594 s.onselect = function(v) { 14595 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14596 }; 14597 } 14598 14599 s = extend({ 14600 title : s.title, 14601 'class' : 'mce_' + id, 14602 scope : s.scope, 14603 control_manager : t 14604 }, s); 14605 14606 id = t.prefix + id; 14607 cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; 14608 c = t.add(new cls(id, s, ed)); 14609 ed.onMouseDown.add(c.hideMenu, c); 14610 14611 return c; 14612 }, 14613 14614 createColorSplitButton : function(id, s, cc) { 14615 var t = this, ed = t.editor, cmd, c, cls, bm; 14616 14617 if (t.get(id)) 14618 return null; 14619 14620 s.title = ed.translate(s.title); 14621 s.scope = s.scope || ed; 14622 14623 if (!s.onclick) { 14624 s.onclick = function(v) { 14625 if (tinymce.isIE) 14626 bm = ed.selection.getBookmark(1); 14627 14628 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14629 }; 14630 } 14631 14632 if (!s.onselect) { 14633 s.onselect = function(v) { 14634 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14635 }; 14636 } 14637 14638 s = extend({ 14639 title : s.title, 14640 'class' : 'mce_' + id, 14641 'menu_class' : ed.getParam('skin') + 'Skin', 14642 scope : s.scope, 14643 more_colors_title : ed.getLang('more_colors') 14644 }, s); 14645 14646 id = t.prefix + id; 14647 cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; 14648 c = new cls(id, s, ed); 14649 ed.onMouseDown.add(c.hideMenu, c); 14650 14651 // Remove the menu element when the editor is removed 14652 ed.onRemove.add(function() { 14653 c.destroy(); 14654 }); 14655 14656 // Fix for bug #1897785, #1898007 14657 if (tinymce.isIE) { 14658 c.onShowMenu.add(function() { 14659 // IE 8 needs focus in order to store away a range with the current collapsed caret location 14660 ed.focus(); 14661 bm = ed.selection.getBookmark(1); 14662 }); 14663 14664 c.onHideMenu.add(function() { 14665 if (bm) { 14666 ed.selection.moveToBookmark(bm); 14667 bm = 0; 14668 } 14669 }); 14670 } 14671 14672 return t.add(c); 14673 }, 14674 14675 createToolbar : function(id, s, cc) { 14676 var c, t = this, cls; 14677 14678 id = t.prefix + id; 14679 cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; 14680 c = new cls(id, s, t.editor); 14681 14682 if (t.get(id)) 14683 return null; 14684 14685 return t.add(c); 14686 }, 14687 14688 createToolbarGroup : function(id, s, cc) { 14689 var c, t = this, cls; 14690 id = t.prefix + id; 14691 cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup; 14692 c = new cls(id, s, t.editor); 14693 14694 if (t.get(id)) 14695 return null; 14696 14697 return t.add(c); 14698 }, 14699 14700 createSeparator : function(cc) { 14701 var cls = cc || this._cls.separator || tinymce.ui.Separator; 14702 14703 return new cls(); 14704 }, 14705 14706 setControlType : function(n, c) { 14707 return this._cls[n.toLowerCase()] = c; 14708 }, 14709 14710 destroy : function() { 14711 each(this.controls, function(c) { 14712 c.destroy(); 14713 }); 14714 14715 this.controls = null; 14716 } 14717 }); 14718 })(tinymce); 14719 14720 (function(tinymce) { 14721 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera; 14722 14723 tinymce.create('tinymce.WindowManager', { 14724 WindowManager : function(ed) { 14725 var t = this; 14726 14727 t.editor = ed; 14728 t.onOpen = new Dispatcher(t); 14729 t.onClose = new Dispatcher(t); 14730 t.params = {}; 14731 t.features = {}; 14732 }, 14733 14734 open : function(s, p) { 14735 var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u; 14736 14737 // Default some options 14738 s = s || {}; 14739 p = p || {}; 14740 sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window 14741 sh = isOpera ? vp.h : screen.height; 14742 s.name = s.name || 'mc_' + new Date().getTime(); 14743 s.width = parseInt(s.width || 320); 14744 s.height = parseInt(s.height || 240); 14745 s.resizable = true; 14746 s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0); 14747 s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0); 14748 p.inline = false; 14749 p.mce_width = s.width; 14750 p.mce_height = s.height; 14751 p.mce_auto_focus = s.auto_focus; 14752 14753 if (mo) { 14754 if (isIE) { 14755 s.center = true; 14756 s.help = false; 14757 s.dialogWidth = s.width + 'px'; 14758 s.dialogHeight = s.height + 'px'; 14759 s.scroll = s.scrollbars || false; 14760 } 14761 } 14762 14763 // Build features string 14764 each(s, function(v, k) { 14765 if (tinymce.is(v, 'boolean')) 14766 v = v ? 'yes' : 'no'; 14767 14768 if (!/^(name|url)$/.test(k)) { 14769 if (isIE && mo) 14770 f += (f ? ';' : '') + k + ':' + v; 14771 else 14772 f += (f ? ',' : '') + k + '=' + v; 14773 } 14774 }); 14775 14776 t.features = s; 14777 t.params = p; 14778 t.onOpen.dispatch(t, s, p); 14779 14780 u = s.url || s.file; 14781 u = tinymce._addVer(u); 14782 14783 try { 14784 if (isIE && mo) { 14785 w = 1; 14786 window.showModalDialog(u, window, f); 14787 } else 14788 w = window.open(u, s.name, f); 14789 } catch (ex) { 14790 // Ignore 14791 } 14792 14793 if (!w) 14794 alert(t.editor.getLang('popup_blocked')); 14795 }, 14796 14797 close : function(w) { 14798 w.close(); 14799 this.onClose.dispatch(this); 14800 }, 14801 14802 createInstance : function(cl, a, b, c, d, e) { 14803 var f = tinymce.resolve(cl); 14804 14805 return new f(a, b, c, d, e); 14806 }, 14807 14808 confirm : function(t, cb, s, w) { 14809 w = w || window; 14810 14811 cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t)))); 14812 }, 14813 14814 alert : function(tx, cb, s, w) { 14815 var t = this; 14816 14817 w = w || window; 14818 w.alert(t._decode(t.editor.getLang(tx, tx))); 14819 14820 if (cb) 14821 cb.call(s || t); 14822 }, 14823 14824 resizeBy : function(dw, dh, win) { 14825 win.resizeBy(dw, dh); 14826 }, 14827 14828 // Internal functions 14829 14830 _decode : function(s) { 14831 return tinymce.DOM.decode(s).replace(/\\n/g, '\n'); 14832 } 14833 }); 14834 }(tinymce)); 14835 (function(tinymce) { 14836 tinymce.Formatter = function(ed) { 14837 var formats = {}, 14838 each = tinymce.each, 14839 dom = ed.dom, 14840 selection = ed.selection, 14841 TreeWalker = tinymce.dom.TreeWalker, 14842 rangeUtils = new tinymce.dom.RangeUtils(dom), 14843 isValid = ed.schema.isValidChild, 14844 isBlock = dom.isBlock, 14845 forcedRootBlock = ed.settings.forced_root_block, 14846 nodeIndex = dom.nodeIndex, 14847 INVISIBLE_CHAR = tinymce.isGecko ? '\u200B' : '\uFEFF', 14848 MCE_ATTR_RE = /^(src|href|style)$/, 14849 FALSE = false, 14850 TRUE = true, 14851 formatChangeData, 14852 undef, 14853 getContentEditable = dom.getContentEditable; 14854 14855 function isArray(obj) { 14856 return obj instanceof Array; 14857 }; 14858 14859 function getParents(node, selector) { 14860 return dom.getParents(node, selector, dom.getRoot()); 14861 }; 14862 14863 function isCaretNode(node) { 14864 return node.nodeType === 1 && node.id === '_mce_caret'; 14865 }; 14866 14867 function defaultFormats() { 14868 register({ 14869 alignleft : [ 14870 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}, defaultBlock: 'div'}, 14871 {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}} 14872 ], 14873 14874 aligncenter : [ 14875 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}, defaultBlock: 'div'}, 14876 {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, 14877 {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}} 14878 ], 14879 14880 alignright : [ 14881 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}, defaultBlock: 'div'}, 14882 {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}} 14883 ], 14884 14885 alignfull : [ 14886 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}, defaultBlock: 'div'} 14887 ], 14888 14889 bold : [ 14890 {inline : 'strong', remove : 'all'}, 14891 {inline : 'span', styles : {fontWeight : 'bold'}}, 14892 {inline : 'b', remove : 'all'} 14893 ], 14894 14895 italic : [ 14896 {inline : 'em', remove : 'all'}, 14897 {inline : 'span', styles : {fontStyle : 'italic'}}, 14898 {inline : 'i', remove : 'all'} 14899 ], 14900 14901 underline : [ 14902 {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, 14903 {inline : 'u', remove : 'all'} 14904 ], 14905 14906 strikethrough : [ 14907 {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, 14908 {inline : 'strike', remove : 'all'} 14909 ], 14910 14911 forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false}, 14912 hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false}, 14913 fontname : {inline : 'span', styles : {fontFamily : '%value'}}, 14914 fontsize : {inline : 'span', styles : {fontSize : '%value'}}, 14915 fontsize_class : {inline : 'span', attributes : {'class' : '%value'}}, 14916 blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'}, 14917 subscript : {inline : 'sub'}, 14918 superscript : {inline : 'sup'}, 14919 14920 link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, 14921 onmatch : function(node) { 14922 return true; 14923 }, 14924 14925 onformat : function(elm, fmt, vars) { 14926 each(vars, function(value, key) { 14927 dom.setAttrib(elm, key, value); 14928 }); 14929 } 14930 }, 14931 14932 removeformat : [ 14933 {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true}, 14934 {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true}, 14935 {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true} 14936 ] 14937 }); 14938 14939 // Register default block formats 14940 each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) { 14941 register(name, {block : name, remove : 'all'}); 14942 }); 14943 14944 // Register user defined formats 14945 register(ed.settings.formats); 14946 }; 14947 14948 function addKeyboardShortcuts() { 14949 // Add some inline shortcuts 14950 ed.addShortcut('ctrl+b', 'bold_desc', 'Bold'); 14951 ed.addShortcut('ctrl+i', 'italic_desc', 'Italic'); 14952 ed.addShortcut('ctrl+u', 'underline_desc', 'Underline'); 14953 14954 // BlockFormat shortcuts keys 14955 for (var i = 1; i <= 6; i++) { 14956 ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); 14957 } 14958 14959 ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); 14960 ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); 14961 ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); 14962 }; 14963 14964 // Public functions 14965 14966 function get(name) { 14967 return name ? formats[name] : formats; 14968 }; 14969 14970 function register(name, format) { 14971 if (name) { 14972 if (typeof(name) !== 'string') { 14973 each(name, function(format, name) { 14974 register(name, format); 14975 }); 14976 } else { 14977 // Force format into array and add it to internal collection 14978 format = format.length ? format : [format]; 14979 14980 each(format, function(format) { 14981 // Set deep to false by default on selector formats this to avoid removing 14982 // alignment on images inside paragraphs when alignment is changed on paragraphs 14983 if (format.deep === undef) 14984 format.deep = !format.selector; 14985 14986 // Default to true 14987 if (format.split === undef) 14988 format.split = !format.selector || format.inline; 14989 14990 // Default to true 14991 if (format.remove === undef && format.selector && !format.inline) 14992 format.remove = 'none'; 14993 14994 // Mark format as a mixed format inline + block level 14995 if (format.selector && format.inline) { 14996 format.mixed = true; 14997 format.block_expand = true; 14998 } 14999 15000 // Split classes if needed 15001 if (typeof(format.classes) === 'string') 15002 format.classes = format.classes.split(/\s+/); 15003 }); 15004 15005 formats[name] = format; 15006 } 15007 } 15008 }; 15009 15010 var getTextDecoration = function(node) { 15011 var decoration; 15012 15013 ed.dom.getParent(node, function(n) { 15014 decoration = ed.dom.getStyle(n, 'text-decoration'); 15015 return decoration && decoration !== 'none'; 15016 }); 15017 15018 return decoration; 15019 }; 15020 15021 var processUnderlineAndColor = function(node) { 15022 var textDecoration; 15023 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { 15024 textDecoration = getTextDecoration(node.parentNode); 15025 if (ed.dom.getStyle(node, 'color') && textDecoration) { 15026 ed.dom.setStyle(node, 'text-decoration', textDecoration); 15027 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { 15028 ed.dom.setStyle(node, 'text-decoration', null); 15029 } 15030 } 15031 }; 15032 15033 function apply(name, vars, node) { 15034 var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed(); 15035 15036 function setElementFormat(elm, fmt) { 15037 fmt = fmt || format; 15038 15039 if (elm) { 15040 if (fmt.onformat) { 15041 fmt.onformat(elm, fmt, vars, node); 15042 } 15043 15044 each(fmt.styles, function(value, name) { 15045 dom.setStyle(elm, name, replaceVars(value, vars)); 15046 }); 15047 15048 each(fmt.attributes, function(value, name) { 15049 dom.setAttrib(elm, name, replaceVars(value, vars)); 15050 }); 15051 15052 each(fmt.classes, function(value) { 15053 value = replaceVars(value, vars); 15054 15055 if (!dom.hasClass(elm, value)) 15056 dom.addClass(elm, value); 15057 }); 15058 } 15059 }; 15060 function adjustSelectionToVisibleSelection() { 15061 function findSelectionEnd(start, end) { 15062 var walker = new TreeWalker(end); 15063 for (node = walker.current(); node; node = walker.prev()) { 15064 if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { 15065 return node; 15066 } 15067 } 15068 }; 15069 15070 // Adjust selection so that a end container with a end offset of zero is not included in the selection 15071 // as this isn't visible to the user. 15072 var rng = ed.selection.getRng(); 15073 var start = rng.startContainer; 15074 var end = rng.endContainer; 15075 15076 if (start != end && rng.endOffset === 0) { 15077 var newEnd = findSelectionEnd(start, end); 15078 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; 15079 15080 rng.setEnd(newEnd, endOffset); 15081 } 15082 15083 return rng; 15084 } 15085 15086 function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ 15087 var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; 15088 15089 // find the index of the first child list. 15090 each(node.childNodes, function(n, index) { 15091 if (n.nodeName === "UL" || n.nodeName === "OL") { 15092 listIndex = index; 15093 list = n; 15094 return false; 15095 } 15096 }); 15097 15098 // get the index of the bookmarks 15099 each(node.childNodes, function(n, index) { 15100 if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { 15101 if (n.id == bookmark.id + "_start") { 15102 startIndex = index; 15103 } else if (n.id == bookmark.id + "_end") { 15104 endIndex = index; 15105 } 15106 } 15107 }); 15108 15109 // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally 15110 if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { 15111 each(tinymce.grep(node.childNodes), process); 15112 return 0; 15113 } else { 15114 currentWrapElm = dom.clone(wrapElm, FALSE); 15115 15116 // create a list of the nodes on the same side of the list as the selection 15117 each(tinymce.grep(node.childNodes), function(n, index) { 15118 if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { 15119 nodes.push(n); 15120 n.parentNode.removeChild(n); 15121 } 15122 }); 15123 15124 // insert the wrapping element either before or after the list. 15125 if (startIndex < listIndex) { 15126 node.insertBefore(currentWrapElm, list); 15127 } else if (startIndex > listIndex) { 15128 node.insertBefore(currentWrapElm, list.nextSibling); 15129 } 15130 15131 // add the new nodes to the list. 15132 newWrappers.push(currentWrapElm); 15133 15134 each(nodes, function(node) { 15135 currentWrapElm.appendChild(node); 15136 }); 15137 15138 return currentWrapElm; 15139 } 15140 }; 15141 15142 function applyRngStyle(rng, bookmark, node_specific) { 15143 var newWrappers = [], wrapName, wrapElm, contentEditable = true; 15144 15145 // Setup wrapper element 15146 wrapName = format.inline || format.block; 15147 wrapElm = dom.create(wrapName); 15148 setElementFormat(wrapElm); 15149 15150 rangeUtils.walk(rng, function(nodes) { 15151 var currentWrapElm; 15152 15153 function process(node) { 15154 var nodeName, parentName, found, hasContentEditableState, lastContentEditable; 15155 15156 lastContentEditable = contentEditable; 15157 nodeName = node.nodeName.toLowerCase(); 15158 parentName = node.parentNode.nodeName.toLowerCase(); 15159 15160 // Node has a contentEditable value 15161 if (node.nodeType === 1 && getContentEditable(node)) { 15162 lastContentEditable = contentEditable; 15163 contentEditable = getContentEditable(node) === "true"; 15164 hasContentEditableState = true; // We don't want to wrap the container only it's children 15165 } 15166 15167 // Stop wrapping on br elements 15168 if (isEq(nodeName, 'br')) { 15169 currentWrapElm = 0; 15170 15171 // Remove any br elements when we wrap things 15172 if (format.block) 15173 dom.remove(node); 15174 15175 return; 15176 } 15177 15178 // If node is wrapper type 15179 if (format.wrapper && matchNode(node, name, vars)) { 15180 currentWrapElm = 0; 15181 return; 15182 } 15183 15184 // Can we rename the block 15185 if (contentEditable && !hasContentEditableState && format.block && !format.wrapper && isTextBlock(nodeName)) { 15186 node = dom.rename(node, wrapName); 15187 setElementFormat(node); 15188 newWrappers.push(node); 15189 currentWrapElm = 0; 15190 return; 15191 } 15192 15193 // Handle selector patterns 15194 if (format.selector) { 15195 // Look for matching formats 15196 each(formatList, function(format) { 15197 // Check collapsed state if it exists 15198 if ('collapsed' in format && format.collapsed !== isCollapsed) { 15199 return; 15200 } 15201 15202 if (dom.is(node, format.selector) && !isCaretNode(node)) { 15203 setElementFormat(node, format); 15204 found = true; 15205 } 15206 }); 15207 15208 // Continue processing if a selector match wasn't found and a inline element is defined 15209 if (!format.inline || found) { 15210 currentWrapElm = 0; 15211 return; 15212 } 15213 } 15214 15215 // Is it valid to wrap this item 15216 if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && 15217 !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node)) { 15218 // Start wrapping 15219 if (!currentWrapElm) { 15220 // Wrap the node 15221 currentWrapElm = dom.clone(wrapElm, FALSE); 15222 node.parentNode.insertBefore(currentWrapElm, node); 15223 newWrappers.push(currentWrapElm); 15224 } 15225 15226 currentWrapElm.appendChild(node); 15227 } else if (nodeName == 'li' && bookmark) { 15228 // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element. 15229 currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); 15230 } else { 15231 // Start a new wrapper for possible children 15232 currentWrapElm = 0; 15233 15234 each(tinymce.grep(node.childNodes), process); 15235 15236 if (hasContentEditableState) { 15237 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 15238 } 15239 15240 // End the last wrapper 15241 currentWrapElm = 0; 15242 } 15243 }; 15244 15245 // Process siblings from range 15246 each(nodes, process); 15247 }); 15248 15249 // Wrap links inside as well, for example color inside a link when the wrapper is around the link 15250 if (format.wrap_links === false) { 15251 each(newWrappers, function(node) { 15252 function process(node) { 15253 var i, currentWrapElm, children; 15254 15255 if (node.nodeName === 'A') { 15256 currentWrapElm = dom.clone(wrapElm, FALSE); 15257 newWrappers.push(currentWrapElm); 15258 15259 children = tinymce.grep(node.childNodes); 15260 for (i = 0; i < children.length; i++) 15261 currentWrapElm.appendChild(children[i]); 15262 15263 node.appendChild(currentWrapElm); 15264 } 15265 15266 each(tinymce.grep(node.childNodes), process); 15267 }; 15268 15269 process(node); 15270 }); 15271 } 15272 15273 // Cleanup 15274 15275 each(newWrappers, function(node) { 15276 var childCount; 15277 15278 function getChildCount(node) { 15279 var count = 0; 15280 15281 each(node.childNodes, function(node) { 15282 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) 15283 count++; 15284 }); 15285 15286 return count; 15287 }; 15288 15289 function mergeStyles(node) { 15290 var child, clone; 15291 15292 each(node.childNodes, function(node) { 15293 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { 15294 child = node; 15295 return FALSE; // break loop 15296 } 15297 }); 15298 15299 // If child was found and of the same type as the current node 15300 if (child && matchName(child, format)) { 15301 clone = dom.clone(child, FALSE); 15302 setElementFormat(clone); 15303 15304 dom.replace(clone, node, TRUE); 15305 dom.remove(child, 1); 15306 } 15307 15308 return clone || node; 15309 }; 15310 15311 childCount = getChildCount(node); 15312 15313 // Remove empty nodes but only if there is multiple wrappers and they are not block 15314 // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at 15315 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { 15316 dom.remove(node, 1); 15317 return; 15318 } 15319 15320 if (format.inline || format.wrapper) { 15321 // Merges the current node with it's children of similar type to reduce the number of elements 15322 if (!format.exact && childCount === 1) 15323 node = mergeStyles(node); 15324 15325 // Remove/merge children 15326 each(formatList, function(format) { 15327 // Merge all children of similar type will move styles from child to parent 15328 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span> 15329 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span> 15330 each(dom.select(format.inline, node), function(child) { 15331 var parent; 15332 15333 // When wrap_links is set to false we don't want 15334 // to remove the format on children within links 15335 if (format.wrap_links === false) { 15336 parent = child.parentNode; 15337 15338 do { 15339 if (parent.nodeName === 'A') 15340 return; 15341 } while (parent = parent.parentNode); 15342 } 15343 15344 removeFormat(format, vars, child, format.exact ? child : null); 15345 }); 15346 }); 15347 15348 // Remove child if direct parent is of same type 15349 if (matchNode(node.parentNode, name, vars)) { 15350 dom.remove(node, 1); 15351 node = 0; 15352 return TRUE; 15353 } 15354 15355 // Look for parent with similar style format 15356 if (format.merge_with_parents) { 15357 dom.getParent(node.parentNode, function(parent) { 15358 if (matchNode(parent, name, vars)) { 15359 dom.remove(node, 1); 15360 node = 0; 15361 return TRUE; 15362 } 15363 }); 15364 } 15365 15366 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b> 15367 if (node && format.merge_siblings !== false) { 15368 node = mergeSiblings(getNonWhiteSpaceSibling(node), node); 15369 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); 15370 } 15371 } 15372 }); 15373 }; 15374 15375 if (format) { 15376 if (node) { 15377 if (node.nodeType) { 15378 rng = dom.createRng(); 15379 rng.setStartBefore(node); 15380 rng.setEndAfter(node); 15381 applyRngStyle(expandRng(rng, formatList), null, true); 15382 } else { 15383 applyRngStyle(node, null, true); 15384 } 15385 } else { 15386 if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { 15387 // Obtain selection node before selection is unselected by applyRngStyle() 15388 var curSelNode = ed.selection.getNode(); 15389 15390 // If the formats have a default block and we can't find a parent block then start wrapping it with a DIV this is for forced_root_blocks: false 15391 // It's kind of a hack but people should be using the default block type P since all desktop editors work that way 15392 if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { 15393 apply(formatList[0].defaultBlock); 15394 } 15395 15396 // Apply formatting to selection 15397 ed.selection.setRng(adjustSelectionToVisibleSelection()); 15398 bookmark = selection.getBookmark(); 15399 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); 15400 15401 // Colored nodes should be underlined so that the color of the underline matches the text color. 15402 if (format.styles && (format.styles.color || format.styles.textDecoration)) { 15403 tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes'); 15404 processUnderlineAndColor(curSelNode); 15405 } 15406 15407 selection.moveToBookmark(bookmark); 15408 moveStart(selection.getRng(TRUE)); 15409 ed.nodeChanged(); 15410 } else 15411 performCaretAction('apply', name, vars); 15412 } 15413 } 15414 }; 15415 15416 function remove(name, vars, node) { 15417 var formatList = get(name), format = formatList[0], bookmark, i, rng, contentEditable = true; 15418 15419 // Merges the styles for each node 15420 function process(node) { 15421 var children, i, l, localContentEditable, lastContentEditable, hasContentEditableState; 15422 15423 // Node has a contentEditable value 15424 if (node.nodeType === 1 && getContentEditable(node)) { 15425 lastContentEditable = contentEditable; 15426 contentEditable = getContentEditable(node) === "true"; 15427 hasContentEditableState = true; // We don't want to wrap the container only it's children 15428 } 15429 15430 // Grab the children first since the nodelist might be changed 15431 children = tinymce.grep(node.childNodes); 15432 15433 // Process current node 15434 if (contentEditable && !hasContentEditableState) { 15435 for (i = 0, l = formatList.length; i < l; i++) { 15436 if (removeFormat(formatList[i], vars, node, node)) 15437 break; 15438 } 15439 } 15440 15441 // Process the children 15442 if (format.deep) { 15443 if (children.length) { 15444 for (i = 0, l = children.length; i < l; i++) 15445 process(children[i]); 15446 15447 if (hasContentEditableState) { 15448 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 15449 } 15450 } 15451 } 15452 }; 15453 15454 function findFormatRoot(container) { 15455 var formatRoot; 15456 15457 // Find format root 15458 each(getParents(container.parentNode).reverse(), function(parent) { 15459 var format; 15460 15461 // Find format root element 15462 if (!formatRoot && parent.id != '_start' && parent.id != '_end') { 15463 // Is the node matching the format we are looking for 15464 format = matchNode(parent, name, vars); 15465 if (format && format.split !== false) 15466 formatRoot = parent; 15467 } 15468 }); 15469 15470 return formatRoot; 15471 }; 15472 15473 function wrapAndSplit(format_root, container, target, split) { 15474 var parent, clone, lastClone, firstClone, i, formatRootParent; 15475 15476 // Format root found then clone formats and split it 15477 if (format_root) { 15478 formatRootParent = format_root.parentNode; 15479 15480 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { 15481 clone = dom.clone(parent, FALSE); 15482 15483 for (i = 0; i < formatList.length; i++) { 15484 if (removeFormat(formatList[i], vars, clone, clone)) { 15485 clone = 0; 15486 break; 15487 } 15488 } 15489 15490 // Build wrapper node 15491 if (clone) { 15492 if (lastClone) 15493 clone.appendChild(lastClone); 15494 15495 if (!firstClone) 15496 firstClone = clone; 15497 15498 lastClone = clone; 15499 } 15500 } 15501 15502 // Never split block elements if the format is mixed 15503 if (split && (!format.mixed || !isBlock(format_root))) 15504 container = dom.split(format_root, container); 15505 15506 // Wrap container in cloned formats 15507 if (lastClone) { 15508 target.parentNode.insertBefore(lastClone, target); 15509 firstClone.appendChild(target); 15510 } 15511 } 15512 15513 return container; 15514 }; 15515 15516 function splitToFormatRoot(container) { 15517 return wrapAndSplit(findFormatRoot(container), container, container, true); 15518 }; 15519 15520 function unwrap(start) { 15521 var node = dom.get(start ? '_start' : '_end'), 15522 out = node[start ? 'firstChild' : 'lastChild']; 15523 15524 // If the end is placed within the start the result will be removed 15525 // So this checks if the out node is a bookmark node if it is it 15526 // checks for another more suitable node 15527 if (isBookmarkNode(out)) 15528 out = out[start ? 'firstChild' : 'lastChild']; 15529 15530 dom.remove(node, true); 15531 15532 return out; 15533 }; 15534 15535 function removeRngStyle(rng) { 15536 var startContainer, endContainer, node; 15537 15538 rng = expandRng(rng, formatList, TRUE); 15539 15540 if (format.split) { 15541 startContainer = getContainer(rng, TRUE); 15542 endContainer = getContainer(rng); 15543 15544 if (startContainer != endContainer) { 15545 // WebKit will render the table incorrectly if we wrap a TD in a SPAN so lets see if the can use the first child instead 15546 // This will happen if you tripple click a table cell and use remove formatting 15547 if (/^(TR|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { 15548 startContainer = (startContainer.nodeName == "TD" ? startContainer.firstChild : startContainer.firstChild.firstChild) || startContainer; 15549 } 15550 15551 // Wrap start/end nodes in span element since these might be cloned/moved 15552 startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'}); 15553 endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'}); 15554 15555 // Split start/end 15556 splitToFormatRoot(startContainer); 15557 splitToFormatRoot(endContainer); 15558 15559 // Unwrap start/end to get real elements again 15560 startContainer = unwrap(TRUE); 15561 endContainer = unwrap(); 15562 } else 15563 startContainer = endContainer = splitToFormatRoot(startContainer); 15564 15565 // Update range positions since they might have changed after the split operations 15566 rng.startContainer = startContainer.parentNode; 15567 rng.startOffset = nodeIndex(startContainer); 15568 rng.endContainer = endContainer.parentNode; 15569 rng.endOffset = nodeIndex(endContainer) + 1; 15570 } 15571 15572 // Remove items between start/end 15573 rangeUtils.walk(rng, function(nodes) { 15574 each(nodes, function(node) { 15575 process(node); 15576 15577 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. 15578 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { 15579 removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node); 15580 } 15581 }); 15582 }); 15583 }; 15584 15585 // Handle node 15586 if (node) { 15587 if (node.nodeType) { 15588 rng = dom.createRng(); 15589 rng.setStartBefore(node); 15590 rng.setEndAfter(node); 15591 removeRngStyle(rng); 15592 } else { 15593 removeRngStyle(node); 15594 } 15595 15596 return; 15597 } 15598 15599 if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { 15600 bookmark = selection.getBookmark(); 15601 removeRngStyle(selection.getRng(TRUE)); 15602 selection.moveToBookmark(bookmark); 15603 15604 // Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node 15605 if (format.inline && match(name, vars, selection.getStart())) { 15606 moveStart(selection.getRng(true)); 15607 } 15608 15609 ed.nodeChanged(); 15610 } else 15611 performCaretAction('remove', name, vars); 15612 }; 15613 15614 function toggle(name, vars, node) { 15615 var fmt = get(name); 15616 15617 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) 15618 remove(name, vars, node); 15619 else 15620 apply(name, vars, node); 15621 }; 15622 15623 function matchNode(node, name, vars, similar) { 15624 var formatList = get(name), format, i, classes; 15625 15626 function matchItems(node, format, item_name) { 15627 var key, value, items = format[item_name], i; 15628 15629 // Custom match 15630 if (format.onmatch) { 15631 return format.onmatch(node, format, item_name); 15632 } 15633 15634 // Check all items 15635 if (items) { 15636 // Non indexed object 15637 if (items.length === undef) { 15638 for (key in items) { 15639 if (items.hasOwnProperty(key)) { 15640 if (item_name === 'attributes') 15641 value = dom.getAttrib(node, key); 15642 else 15643 value = getStyle(node, key); 15644 15645 if (similar && !value && !format.exact) 15646 return; 15647 15648 if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars))) 15649 return; 15650 } 15651 } 15652 } else { 15653 // Only one match needed for indexed arrays 15654 for (i = 0; i < items.length; i++) { 15655 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) 15656 return format; 15657 } 15658 } 15659 } 15660 15661 return format; 15662 }; 15663 15664 if (formatList && node) { 15665 // Check each format in list 15666 for (i = 0; i < formatList.length; i++) { 15667 format = formatList[i]; 15668 15669 // Name name, attributes, styles and classes 15670 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { 15671 // Match classes 15672 if (classes = format.classes) { 15673 for (i = 0; i < classes.length; i++) { 15674 if (!dom.hasClass(node, classes[i])) 15675 return; 15676 } 15677 } 15678 15679 return format; 15680 } 15681 } 15682 } 15683 }; 15684 15685 function match(name, vars, node) { 15686 var startNode; 15687 15688 function matchParents(node) { 15689 // Find first node with similar format settings 15690 node = dom.getParent(node, function(node) { 15691 return !!matchNode(node, name, vars, true); 15692 }); 15693 15694 // Do an exact check on the similar format element 15695 return matchNode(node, name, vars); 15696 }; 15697 15698 // Check specified node 15699 if (node) 15700 return matchParents(node); 15701 15702 // Check selected node 15703 node = selection.getNode(); 15704 if (matchParents(node)) 15705 return TRUE; 15706 15707 // Check start node if it's different 15708 startNode = selection.getStart(); 15709 if (startNode != node) { 15710 if (matchParents(startNode)) 15711 return TRUE; 15712 } 15713 15714 return FALSE; 15715 }; 15716 15717 function matchAll(names, vars) { 15718 var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; 15719 15720 // Check start of selection for formats 15721 startElement = selection.getStart(); 15722 dom.getParent(startElement, function(node) { 15723 var i, name; 15724 15725 for (i = 0; i < names.length; i++) { 15726 name = names[i]; 15727 15728 if (!checkedMap[name] && matchNode(node, name, vars)) { 15729 checkedMap[name] = true; 15730 matchedFormatNames.push(name); 15731 } 15732 } 15733 }, dom.getRoot()); 15734 15735 return matchedFormatNames; 15736 }; 15737 15738 function canApply(name) { 15739 var formatList = get(name), startNode, parents, i, x, selector; 15740 15741 if (formatList) { 15742 startNode = selection.getStart(); 15743 parents = getParents(startNode); 15744 15745 for (x = formatList.length - 1; x >= 0; x--) { 15746 selector = formatList[x].selector; 15747 15748 // Format is not selector based, then always return TRUE 15749 if (!selector) 15750 return TRUE; 15751 15752 for (i = parents.length - 1; i >= 0; i--) { 15753 if (dom.is(parents[i], selector)) 15754 return TRUE; 15755 } 15756 } 15757 } 15758 15759 return FALSE; 15760 }; 15761 15762 function formatChanged(formats, callback) { 15763 var currentFormats; 15764 15765 // Setup format node change logic 15766 if (!formatChangeData) { 15767 formatChangeData = {}; 15768 currentFormats = {}; 15769 15770 ed.onNodeChange.addToTop(function(ed, cm, node) { 15771 var parents = getParents(node), matchedFormats = {}; 15772 15773 // Check for new formats 15774 each(formatChangeData, function(callbacks, format) { 15775 each(parents, function(node) { 15776 if (matchNode(node, format, {}, true)) { 15777 if (!currentFormats[format]) { 15778 // Execute callbacks 15779 each(callbacks, function(callback) { 15780 callback(true, {node: node, format: format, parents: parents}); 15781 }); 15782 15783 currentFormats[format] = callbacks; 15784 } 15785 15786 matchedFormats[format] = callbacks; 15787 return false; 15788 } 15789 }); 15790 }); 15791 15792 // Check if current formats still match 15793 each(currentFormats, function(callbacks, format) { 15794 if (!matchedFormats[format]) { 15795 delete currentFormats[format]; 15796 15797 each(callbacks, function(callback) { 15798 callback(false, {node: node, format: format, parents: parents}); 15799 }); 15800 } 15801 }); 15802 }); 15803 } 15804 15805 // Add format listeners 15806 each(formats.split(','), function(format) { 15807 if (!formatChangeData[format]) { 15808 formatChangeData[format] = []; 15809 } 15810 15811 formatChangeData[format].push(callback); 15812 }); 15813 15814 return this; 15815 }; 15816 15817 // Expose to public 15818 tinymce.extend(this, { 15819 get : get, 15820 register : register, 15821 apply : apply, 15822 remove : remove, 15823 toggle : toggle, 15824 match : match, 15825 matchAll : matchAll, 15826 matchNode : matchNode, 15827 canApply : canApply, 15828 formatChanged: formatChanged 15829 }); 15830 15831 // Initialize 15832 defaultFormats(); 15833 addKeyboardShortcuts(); 15834 15835 // Private functions 15836 15837 function matchName(node, format) { 15838 // Check for inline match 15839 if (isEq(node, format.inline)) 15840 return TRUE; 15841 15842 // Check for block match 15843 if (isEq(node, format.block)) 15844 return TRUE; 15845 15846 // Check for selector match 15847 if (format.selector) 15848 return dom.is(node, format.selector); 15849 }; 15850 15851 function isEq(str1, str2) { 15852 str1 = str1 || ''; 15853 str2 = str2 || ''; 15854 15855 str1 = '' + (str1.nodeName || str1); 15856 str2 = '' + (str2.nodeName || str2); 15857 15858 return str1.toLowerCase() == str2.toLowerCase(); 15859 }; 15860 15861 function getStyle(node, name) { 15862 var styleVal = dom.getStyle(node, name); 15863 15864 // Force the format to hex 15865 if (name == 'color' || name == 'backgroundColor') 15866 styleVal = dom.toHex(styleVal); 15867 15868 // Opera will return bold as 700 15869 if (name == 'fontWeight' && styleVal == 700) 15870 styleVal = 'bold'; 15871 15872 return '' + styleVal; 15873 }; 15874 15875 function replaceVars(value, vars) { 15876 if (typeof(value) != "string") 15877 value = value(vars); 15878 else if (vars) { 15879 value = value.replace(/%(\w+)/g, function(str, name) { 15880 return vars[name] || str; 15881 }); 15882 } 15883 15884 return value; 15885 }; 15886 15887 function isWhiteSpaceNode(node) { 15888 return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); 15889 }; 15890 15891 function wrap(node, name, attrs) { 15892 var wrapper = dom.create(name, attrs); 15893 15894 node.parentNode.insertBefore(wrapper, node); 15895 wrapper.appendChild(node); 15896 15897 return wrapper; 15898 }; 15899 15900 function expandRng(rng, format, remove) { 15901 var sibling, lastIdx, leaf, endPoint, 15902 startContainer = rng.startContainer, 15903 startOffset = rng.startOffset, 15904 endContainer = rng.endContainer, 15905 endOffset = rng.endOffset; 15906 15907 // This function walks up the tree if there is no siblings before/after the node 15908 function findParentContainer(start) { 15909 var container, parent, child, sibling, siblingName, root; 15910 15911 container = parent = start ? startContainer : endContainer; 15912 siblingName = start ? 'previousSibling' : 'nextSibling'; 15913 root = dom.getRoot(); 15914 15915 // If it's a text node and the offset is inside the text 15916 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { 15917 if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { 15918 return container; 15919 } 15920 } 15921 15922 for (;;) { 15923 // Stop expanding on block elements 15924 if (!format[0].block_expand && isBlock(parent)) 15925 return parent; 15926 15927 // Walk left/right 15928 for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { 15929 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { 15930 return parent; 15931 } 15932 } 15933 15934 // Check if we can move up are we at root level or body level 15935 if (parent.parentNode == root) { 15936 container = parent; 15937 break; 15938 } 15939 15940 parent = parent.parentNode; 15941 } 15942 15943 return container; 15944 }; 15945 15946 // This function walks down the tree to find the leaf at the selection. 15947 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. 15948 function findLeaf(node, offset) { 15949 if (offset === undef) 15950 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 15951 while (node && node.hasChildNodes()) { 15952 node = node.childNodes[offset]; 15953 if (node) 15954 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 15955 } 15956 return { node: node, offset: offset }; 15957 } 15958 15959 // If index based start position then resolve it 15960 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 15961 lastIdx = startContainer.childNodes.length - 1; 15962 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; 15963 15964 if (startContainer.nodeType == 3) 15965 startOffset = 0; 15966 } 15967 15968 // If index based end position then resolve it 15969 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 15970 lastIdx = endContainer.childNodes.length - 1; 15971 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; 15972 15973 if (endContainer.nodeType == 3) 15974 endOffset = endContainer.nodeValue.length; 15975 } 15976 15977 // Expands the node to the closes contentEditable false element if it exists 15978 function findParentContentEditable(node) { 15979 var parent = node; 15980 15981 while (parent) { 15982 if (parent.nodeType === 1 && getContentEditable(parent)) { 15983 return getContentEditable(parent) === "false" ? parent : node; 15984 } 15985 15986 parent = parent.parentNode; 15987 } 15988 15989 return node; 15990 }; 15991 15992 function findWordEndPoint(container, offset, start) { 15993 var walker, node, pos, lastTextNode; 15994 15995 function findSpace(node, offset) { 15996 var pos, pos2, str = node.nodeValue; 15997 15998 if (typeof(offset) == "undefined") { 15999 offset = start ? str.length : 0; 16000 } 16001 16002 if (start) { 16003 pos = str.lastIndexOf(' ', offset); 16004 pos2 = str.lastIndexOf('\u00a0', offset); 16005 pos = pos > pos2 ? pos : pos2; 16006 16007 // Include the space on remove to avoid tag soup 16008 if (pos !== -1 && !remove) { 16009 pos++; 16010 } 16011 } else { 16012 pos = str.indexOf(' ', offset); 16013 pos2 = str.indexOf('\u00a0', offset); 16014 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; 16015 } 16016 16017 return pos; 16018 }; 16019 16020 if (container.nodeType === 3) { 16021 pos = findSpace(container, offset); 16022 16023 if (pos !== -1) { 16024 return {container : container, offset : pos}; 16025 } 16026 16027 lastTextNode = container; 16028 } 16029 16030 // Walk the nodes inside the block 16031 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); 16032 while (node = walker[start ? 'prev' : 'next']()) { 16033 if (node.nodeType === 3) { 16034 lastTextNode = node; 16035 pos = findSpace(node); 16036 16037 if (pos !== -1) { 16038 return {container : node, offset : pos}; 16039 } 16040 } else if (isBlock(node)) { 16041 break; 16042 } 16043 } 16044 16045 if (lastTextNode) { 16046 if (start) { 16047 offset = 0; 16048 } else { 16049 offset = lastTextNode.length; 16050 } 16051 16052 return {container: lastTextNode, offset: offset}; 16053 } 16054 }; 16055 16056 function findSelectorEndPoint(container, sibling_name) { 16057 var parents, i, y, curFormat; 16058 16059 if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) 16060 container = container[sibling_name]; 16061 16062 parents = getParents(container); 16063 for (i = 0; i < parents.length; i++) { 16064 for (y = 0; y < format.length; y++) { 16065 curFormat = format[y]; 16066 16067 // If collapsed state is set then skip formats that doesn't match that 16068 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) 16069 continue; 16070 16071 if (dom.is(parents[i], curFormat.selector)) 16072 return parents[i]; 16073 } 16074 } 16075 16076 return container; 16077 }; 16078 16079 function findBlockEndPoint(container, sibling_name, sibling_name2) { 16080 var node; 16081 16082 // Expand to block of similar type 16083 if (!format[0].wrapper) 16084 node = dom.getParent(container, format[0].block); 16085 16086 // Expand to first wrappable block element or any block element 16087 if (!node) 16088 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock); 16089 16090 // Exclude inner lists from wrapping 16091 if (node && format[0].wrapper) 16092 node = getParents(node, 'ul,ol').reverse()[0] || node; 16093 16094 // Didn't find a block element look for first/last wrappable element 16095 if (!node) { 16096 node = container; 16097 16098 while (node[sibling_name] && !isBlock(node[sibling_name])) { 16099 node = node[sibling_name]; 16100 16101 // Break on BR but include it will be removed later on 16102 // we can't remove it now since we need to check if it can be wrapped 16103 if (isEq(node, 'br')) 16104 break; 16105 } 16106 } 16107 16108 return node || container; 16109 }; 16110 16111 // Expand to closest contentEditable element 16112 startContainer = findParentContentEditable(startContainer); 16113 endContainer = findParentContentEditable(endContainer); 16114 16115 // Exclude bookmark nodes if possible 16116 if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { 16117 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; 16118 startContainer = startContainer.nextSibling || startContainer; 16119 16120 if (startContainer.nodeType == 3) 16121 startOffset = 0; 16122 } 16123 16124 if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { 16125 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; 16126 endContainer = endContainer.previousSibling || endContainer; 16127 16128 if (endContainer.nodeType == 3) 16129 endOffset = endContainer.length; 16130 } 16131 16132 if (format[0].inline) { 16133 if (rng.collapsed) { 16134 // Expand left to closest word boundery 16135 endPoint = findWordEndPoint(startContainer, startOffset, true); 16136 if (endPoint) { 16137 startContainer = endPoint.container; 16138 startOffset = endPoint.offset; 16139 } 16140 16141 // Expand right to closest word boundery 16142 endPoint = findWordEndPoint(endContainer, endOffset); 16143 if (endPoint) { 16144 endContainer = endPoint.container; 16145 endOffset = endPoint.offset; 16146 } 16147 } 16148 16149 // Avoid applying formatting to a trailing space. 16150 leaf = findLeaf(endContainer, endOffset); 16151 if (leaf.node) { 16152 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) 16153 leaf = findLeaf(leaf.node.previousSibling); 16154 16155 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && 16156 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { 16157 16158 if (leaf.offset > 1) { 16159 endContainer = leaf.node; 16160 endContainer.splitText(leaf.offset - 1); 16161 } 16162 } 16163 } 16164 } 16165 16166 // Move start/end point up the tree if the leaves are sharp and if we are in different containers 16167 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! 16168 // This will reduce the number of wrapper elements that needs to be created 16169 // Move start point up the tree 16170 if (format[0].inline || format[0].block_expand) { 16171 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { 16172 startContainer = findParentContainer(true); 16173 } 16174 16175 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { 16176 endContainer = findParentContainer(); 16177 } 16178 } 16179 16180 // Expand start/end container to matching selector 16181 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { 16182 // Find new startContainer/endContainer if there is better one 16183 startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); 16184 endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); 16185 } 16186 16187 // Expand start/end container to matching block element or text node 16188 if (format[0].block || format[0].selector) { 16189 // Find new startContainer/endContainer if there is better one 16190 startContainer = findBlockEndPoint(startContainer, 'previousSibling'); 16191 endContainer = findBlockEndPoint(endContainer, 'nextSibling'); 16192 16193 // Non block element then try to expand up the leaf 16194 if (format[0].block) { 16195 if (!isBlock(startContainer)) 16196 startContainer = findParentContainer(true); 16197 16198 if (!isBlock(endContainer)) 16199 endContainer = findParentContainer(); 16200 } 16201 } 16202 16203 // Setup index for startContainer 16204 if (startContainer.nodeType == 1) { 16205 startOffset = nodeIndex(startContainer); 16206 startContainer = startContainer.parentNode; 16207 } 16208 16209 // Setup index for endContainer 16210 if (endContainer.nodeType == 1) { 16211 endOffset = nodeIndex(endContainer) + 1; 16212 endContainer = endContainer.parentNode; 16213 } 16214 16215 // Return new range like object 16216 return { 16217 startContainer : startContainer, 16218 startOffset : startOffset, 16219 endContainer : endContainer, 16220 endOffset : endOffset 16221 }; 16222 } 16223 16224 function removeFormat(format, vars, node, compare_node) { 16225 var i, attrs, stylesModified; 16226 16227 // Check if node matches format 16228 if (!matchName(node, format)) 16229 return FALSE; 16230 16231 // Should we compare with format attribs and styles 16232 if (format.remove != 'all') { 16233 // Remove styles 16234 each(format.styles, function(value, name) { 16235 value = replaceVars(value, vars); 16236 16237 // Indexed array 16238 if (typeof(name) === 'number') { 16239 name = value; 16240 compare_node = 0; 16241 } 16242 16243 if (!compare_node || isEq(getStyle(compare_node, name), value)) 16244 dom.setStyle(node, name, ''); 16245 16246 stylesModified = 1; 16247 }); 16248 16249 // Remove style attribute if it's empty 16250 if (stylesModified && dom.getAttrib(node, 'style') == '') { 16251 node.removeAttribute('style'); 16252 node.removeAttribute('data-mce-style'); 16253 } 16254 16255 // Remove attributes 16256 each(format.attributes, function(value, name) { 16257 var valueOut; 16258 16259 value = replaceVars(value, vars); 16260 16261 // Indexed array 16262 if (typeof(name) === 'number') { 16263 name = value; 16264 compare_node = 0; 16265 } 16266 16267 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { 16268 // Keep internal classes 16269 if (name == 'class') { 16270 value = dom.getAttrib(node, name); 16271 if (value) { 16272 // Build new class value where everything is removed except the internal prefixed classes 16273 valueOut = ''; 16274 each(value.split(/\s+/), function(cls) { 16275 if (/mce\w+/.test(cls)) 16276 valueOut += (valueOut ? ' ' : '') + cls; 16277 }); 16278 16279 // We got some internal classes left 16280 if (valueOut) { 16281 dom.setAttrib(node, name, valueOut); 16282 return; 16283 } 16284 } 16285 } 16286 16287 // IE6 has a bug where the attribute doesn't get removed correctly 16288 if (name == "class") 16289 node.removeAttribute('className'); 16290 16291 // Remove mce prefixed attributes 16292 if (MCE_ATTR_RE.test(name)) 16293 node.removeAttribute('data-mce-' + name); 16294 16295 node.removeAttribute(name); 16296 } 16297 }); 16298 16299 // Remove classes 16300 each(format.classes, function(value) { 16301 value = replaceVars(value, vars); 16302 16303 if (!compare_node || dom.hasClass(compare_node, value)) 16304 dom.removeClass(node, value); 16305 }); 16306 16307 // Check for non internal attributes 16308 attrs = dom.getAttribs(node); 16309 for (i = 0; i < attrs.length; i++) { 16310 if (attrs[i].nodeName.indexOf('_') !== 0) 16311 return FALSE; 16312 } 16313 } 16314 16315 // Remove the inline child if it's empty for example <b> or <span> 16316 if (format.remove != 'none') { 16317 removeNode(node, format); 16318 return TRUE; 16319 } 16320 }; 16321 16322 function removeNode(node, format) { 16323 var parentNode = node.parentNode, rootBlockElm; 16324 16325 function find(node, next, inc) { 16326 node = getNonWhiteSpaceSibling(node, next, inc); 16327 16328 return !node || (node.nodeName == 'BR' || isBlock(node)); 16329 }; 16330 16331 if (format.block) { 16332 if (!forcedRootBlock) { 16333 // Append BR elements if needed before we remove the block 16334 if (isBlock(node) && !isBlock(parentNode)) { 16335 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) 16336 node.insertBefore(dom.create('br'), node.firstChild); 16337 16338 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) 16339 node.appendChild(dom.create('br')); 16340 } 16341 } else { 16342 // Wrap the block in a forcedRootBlock if we are at the root of document 16343 if (parentNode == dom.getRoot()) { 16344 if (!format.list_block || !isEq(node, format.list_block)) { 16345 each(tinymce.grep(node.childNodes), function(node) { 16346 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { 16347 if (!rootBlockElm) 16348 rootBlockElm = wrap(node, forcedRootBlock); 16349 else 16350 rootBlockElm.appendChild(node); 16351 } else 16352 rootBlockElm = 0; 16353 }); 16354 } 16355 } 16356 } 16357 } 16358 16359 // Never remove nodes that isn't the specified inline element if a selector is specified too 16360 if (format.selector && format.inline && !isEq(format.inline, node)) 16361 return; 16362 16363 dom.remove(node, 1); 16364 }; 16365 16366 function getNonWhiteSpaceSibling(node, next, inc) { 16367 if (node) { 16368 next = next ? 'nextSibling' : 'previousSibling'; 16369 16370 for (node = inc ? node : node[next]; node; node = node[next]) { 16371 if (node.nodeType == 1 || !isWhiteSpaceNode(node)) 16372 return node; 16373 } 16374 } 16375 }; 16376 16377 function isBookmarkNode(node) { 16378 return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; 16379 }; 16380 16381 function mergeSiblings(prev, next) { 16382 var marker, sibling, tmpSibling; 16383 16384 function compareElements(node1, node2) { 16385 // Not the same name 16386 if (node1.nodeName != node2.nodeName) 16387 return FALSE; 16388 16389 function getAttribs(node) { 16390 var attribs = {}; 16391 16392 each(dom.getAttribs(node), function(attr) { 16393 var name = attr.nodeName.toLowerCase(); 16394 16395 // Don't compare internal attributes or style 16396 if (name.indexOf('_') !== 0 && name !== 'style') 16397 attribs[name] = dom.getAttrib(node, name); 16398 }); 16399 16400 return attribs; 16401 }; 16402 16403 function compareObjects(obj1, obj2) { 16404 var value, name; 16405 16406 for (name in obj1) { 16407 // Obj1 has item obj2 doesn't have 16408 if (obj1.hasOwnProperty(name)) { 16409 value = obj2[name]; 16410 16411 // Obj2 doesn't have obj1 item 16412 if (value === undef) 16413 return FALSE; 16414 16415 // Obj2 item has a different value 16416 if (obj1[name] != value) 16417 return FALSE; 16418 16419 // Delete similar value 16420 delete obj2[name]; 16421 } 16422 } 16423 16424 // Check if obj 2 has something obj 1 doesn't have 16425 for (name in obj2) { 16426 // Obj2 has item obj1 doesn't have 16427 if (obj2.hasOwnProperty(name)) 16428 return FALSE; 16429 } 16430 16431 return TRUE; 16432 }; 16433 16434 // Attribs are not the same 16435 if (!compareObjects(getAttribs(node1), getAttribs(node2))) 16436 return FALSE; 16437 16438 // Styles are not the same 16439 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) 16440 return FALSE; 16441 16442 return TRUE; 16443 }; 16444 16445 function findElementSibling(node, sibling_name) { 16446 for (sibling = node; sibling; sibling = sibling[sibling_name]) { 16447 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) 16448 return node; 16449 16450 if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) 16451 return sibling; 16452 } 16453 16454 return node; 16455 }; 16456 16457 // Check if next/prev exists and that they are elements 16458 if (prev && next) { 16459 // If previous sibling is empty then jump over it 16460 prev = findElementSibling(prev, 'previousSibling'); 16461 next = findElementSibling(next, 'nextSibling'); 16462 16463 // Compare next and previous nodes 16464 if (compareElements(prev, next)) { 16465 // Append nodes between 16466 for (sibling = prev.nextSibling; sibling && sibling != next;) { 16467 tmpSibling = sibling; 16468 sibling = sibling.nextSibling; 16469 prev.appendChild(tmpSibling); 16470 } 16471 16472 // Remove next node 16473 dom.remove(next); 16474 16475 // Move children into prev node 16476 each(tinymce.grep(next.childNodes), function(node) { 16477 prev.appendChild(node); 16478 }); 16479 16480 return prev; 16481 } 16482 } 16483 16484 return next; 16485 }; 16486 16487 function isTextBlock(name) { 16488 return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); 16489 }; 16490 16491 function getContainer(rng, start) { 16492 var container, offset, lastIdx, walker; 16493 16494 container = rng[start ? 'startContainer' : 'endContainer']; 16495 offset = rng[start ? 'startOffset' : 'endOffset']; 16496 16497 if (container.nodeType == 1) { 16498 lastIdx = container.childNodes.length - 1; 16499 16500 if (!start && offset) 16501 offset--; 16502 16503 container = container.childNodes[offset > lastIdx ? lastIdx : offset]; 16504 } 16505 16506 // If start text node is excluded then walk to the next node 16507 if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { 16508 container = new TreeWalker(container, ed.getBody()).next() || container; 16509 } 16510 16511 // If end text node is excluded then walk to the previous node 16512 if (container.nodeType === 3 && !start && offset === 0) { 16513 container = new TreeWalker(container, ed.getBody()).prev() || container; 16514 } 16515 16516 return container; 16517 }; 16518 16519 function performCaretAction(type, name, vars) { 16520 var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; 16521 16522 // Creates a caret container bogus element 16523 function createCaretContainer(fill) { 16524 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); 16525 16526 if (fill) { 16527 caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); 16528 } 16529 16530 return caretContainer; 16531 }; 16532 16533 function isCaretContainerEmpty(node, nodes) { 16534 while (node) { 16535 if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { 16536 return false; 16537 } 16538 16539 // Collect nodes 16540 if (nodes && node.nodeType === 1) { 16541 nodes.push(node); 16542 } 16543 16544 node = node.firstChild; 16545 } 16546 16547 return true; 16548 }; 16549 16550 // Returns any parent caret container element 16551 function getParentCaretContainer(node) { 16552 while (node) { 16553 if (node.id === caretContainerId) { 16554 return node; 16555 } 16556 16557 node = node.parentNode; 16558 } 16559 }; 16560 16561 // Finds the first text node in the specified node 16562 function findFirstTextNode(node) { 16563 var walker; 16564 16565 if (node) { 16566 walker = new TreeWalker(node, node); 16567 16568 for (node = walker.current(); node; node = walker.next()) { 16569 if (node.nodeType === 3) { 16570 return node; 16571 } 16572 } 16573 } 16574 }; 16575 16576 // Removes the caret container for the specified node or all on the current document 16577 function removeCaretContainer(node, move_caret) { 16578 var child, rng; 16579 16580 if (!node) { 16581 node = getParentCaretContainer(selection.getStart()); 16582 16583 if (!node) { 16584 while (node = dom.get(caretContainerId)) { 16585 removeCaretContainer(node, false); 16586 } 16587 } 16588 } else { 16589 rng = selection.getRng(true); 16590 16591 if (isCaretContainerEmpty(node)) { 16592 if (move_caret !== false) { 16593 rng.setStartBefore(node); 16594 rng.setEndBefore(node); 16595 } 16596 16597 dom.remove(node); 16598 } else { 16599 child = findFirstTextNode(node); 16600 16601 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { 16602 child = child.deleteData(0, 1); 16603 } 16604 16605 dom.remove(node, 1); 16606 } 16607 16608 selection.setRng(rng); 16609 } 16610 }; 16611 16612 // Applies formatting to the caret postion 16613 function applyCaretFormat() { 16614 var rng, caretContainer, textNode, offset, bookmark, container, text; 16615 16616 rng = selection.getRng(true); 16617 offset = rng.startOffset; 16618 container = rng.startContainer; 16619 text = container.nodeValue; 16620 16621 caretContainer = getParentCaretContainer(selection.getStart()); 16622 if (caretContainer) { 16623 textNode = findFirstTextNode(caretContainer); 16624 } 16625 16626 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character 16627 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { 16628 // Get bookmark of caret position 16629 bookmark = selection.getBookmark(); 16630 16631 // Collapse bookmark range (WebKit) 16632 rng.collapse(true); 16633 16634 // Expand the range to the closest word and split it at those points 16635 rng = expandRng(rng, get(name)); 16636 rng = rangeUtils.split(rng); 16637 16638 // Apply the format to the range 16639 apply(name, vars, rng); 16640 16641 // Move selection back to caret position 16642 selection.moveToBookmark(bookmark); 16643 } else { 16644 if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { 16645 caretContainer = createCaretContainer(true); 16646 textNode = caretContainer.firstChild; 16647 16648 rng.insertNode(caretContainer); 16649 offset = 1; 16650 16651 apply(name, vars, caretContainer); 16652 } else { 16653 apply(name, vars, caretContainer); 16654 } 16655 16656 // Move selection to text node 16657 selection.setCursorLocation(textNode, offset); 16658 } 16659 }; 16660 16661 function removeCaretFormat() { 16662 var rng = selection.getRng(true), container, offset, bookmark, 16663 hasContentAfter, node, formatNode, parents = [], i, caretContainer; 16664 16665 container = rng.startContainer; 16666 offset = rng.startOffset; 16667 node = container; 16668 16669 if (container.nodeType == 3) { 16670 if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) { 16671 hasContentAfter = true; 16672 } 16673 16674 node = node.parentNode; 16675 } 16676 16677 while (node) { 16678 if (matchNode(node, name, vars)) { 16679 formatNode = node; 16680 break; 16681 } 16682 16683 if (node.nextSibling) { 16684 hasContentAfter = true; 16685 } 16686 16687 parents.push(node); 16688 node = node.parentNode; 16689 } 16690 16691 // Node doesn't have the specified format 16692 if (!formatNode) { 16693 return; 16694 } 16695 16696 // Is there contents after the caret then remove the format on the element 16697 if (hasContentAfter) { 16698 // Get bookmark of caret position 16699 bookmark = selection.getBookmark(); 16700 16701 // Collapse bookmark range (WebKit) 16702 rng.collapse(true); 16703 16704 // Expand the range to the closest word and split it at those points 16705 rng = expandRng(rng, get(name), true); 16706 rng = rangeUtils.split(rng); 16707 16708 // Remove the format from the range 16709 remove(name, vars, rng); 16710 16711 // Move selection back to caret position 16712 selection.moveToBookmark(bookmark); 16713 } else { 16714 caretContainer = createCaretContainer(); 16715 16716 node = caretContainer; 16717 for (i = parents.length - 1; i >= 0; i--) { 16718 node.appendChild(dom.clone(parents[i], false)); 16719 node = node.firstChild; 16720 } 16721 16722 // Insert invisible character into inner most format element 16723 node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); 16724 node = node.firstChild; 16725 16726 // Insert caret container after the formated node 16727 dom.insertAfter(caretContainer, formatNode); 16728 16729 // Move selection to text node 16730 selection.setCursorLocation(node, 1); 16731 } 16732 }; 16733 16734 // Checks if the parent caret container node isn't empty if that is the case it 16735 // will remove the bogus state on all children that isn't empty 16736 function unmarkBogusCaretParents() { 16737 var i, caretContainer, node; 16738 16739 caretContainer = getParentCaretContainer(selection.getStart()); 16740 if (caretContainer && !dom.isEmpty(caretContainer)) { 16741 tinymce.walk(caretContainer, function(node) { 16742 if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { 16743 dom.setAttrib(node, 'data-mce-bogus', null); 16744 } 16745 }, 'childNodes'); 16746 } 16747 }; 16748 16749 // Only bind the caret events once 16750 if (!self._hasCaretEvents) { 16751 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements 16752 ed.onBeforeGetContent.addToTop(function() { 16753 var nodes = [], i; 16754 16755 if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { 16756 // Mark children 16757 i = nodes.length; 16758 while (i--) { 16759 dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); 16760 } 16761 } 16762 }); 16763 16764 // Remove caret container on mouse up and on key up 16765 tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { 16766 ed[name].addToTop(function() { 16767 removeCaretContainer(); 16768 unmarkBogusCaretParents(); 16769 }); 16770 }); 16771 16772 // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys 16773 ed.onKeyDown.addToTop(function(ed, e) { 16774 var keyCode = e.keyCode; 16775 16776 if (keyCode == 8 || keyCode == 37 || keyCode == 39) { 16777 removeCaretContainer(getParentCaretContainer(selection.getStart())); 16778 } 16779 16780 unmarkBogusCaretParents(); 16781 }); 16782 16783 // Remove bogus state if they got filled by contents using editor.selection.setContent 16784 selection.onSetContent.add(unmarkBogusCaretParents); 16785 16786 self._hasCaretEvents = true; 16787 } 16788 16789 // Do apply or remove caret format 16790 if (type == "apply") { 16791 applyCaretFormat(); 16792 } else { 16793 removeCaretFormat(); 16794 } 16795 }; 16796 16797 function moveStart(rng) { 16798 var container = rng.startContainer, 16799 offset = rng.startOffset, isAtEndOfText, 16800 walker, node, nodes, tmpNode; 16801 16802 // Convert text node into index if possible 16803 if (container.nodeType == 3 && offset >= container.nodeValue.length) { 16804 // Get the parent container location and walk from there 16805 offset = nodeIndex(container); 16806 container = container.parentNode; 16807 isAtEndOfText = true; 16808 } 16809 16810 // Move startContainer/startOffset in to a suitable node 16811 if (container.nodeType == 1) { 16812 nodes = container.childNodes; 16813 container = nodes[Math.min(offset, nodes.length - 1)]; 16814 walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); 16815 16816 // If offset is at end of the parent node walk to the next one 16817 if (offset > nodes.length - 1 || isAtEndOfText) 16818 walker.next(); 16819 16820 for (node = walker.current(); node; node = walker.next()) { 16821 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { 16822 // IE has a "neat" feature where it moves the start node into the closest element 16823 // we can avoid this by inserting an element before it and then remove it after we set the selection 16824 tmpNode = dom.create('a', null, INVISIBLE_CHAR); 16825 node.parentNode.insertBefore(tmpNode, node); 16826 16827 // Set selection and remove tmpNode 16828 rng.setStart(node, 0); 16829 selection.setRng(rng); 16830 dom.remove(tmpNode); 16831 16832 return; 16833 } 16834 } 16835 } 16836 }; 16837 }; 16838 })(tinymce); 16839 16840 tinymce.onAddEditor.add(function(tinymce, ed) { 16841 var filters, fontSizes, dom, settings = ed.settings; 16842 16843 function replaceWithSpan(node, styles) { 16844 tinymce.each(styles, function(value, name) { 16845 if (value) 16846 dom.setStyle(node, name, value); 16847 }); 16848 16849 dom.rename(node, 'span'); 16850 }; 16851 16852 function convert(editor, params) { 16853 dom = editor.dom; 16854 16855 if (settings.convert_fonts_to_spans) { 16856 tinymce.each(dom.select('font,u,strike', params.node), function(node) { 16857 filters[node.nodeName.toLowerCase()](ed.dom, node); 16858 }); 16859 } 16860 }; 16861 16862 if (settings.inline_styles) { 16863 fontSizes = tinymce.explode(settings.font_size_legacy_values); 16864 16865 filters = { 16866 font : function(dom, node) { 16867 replaceWithSpan(node, { 16868 backgroundColor : node.style.backgroundColor, 16869 color : node.color, 16870 fontFamily : node.face, 16871 fontSize : fontSizes[parseInt(node.size, 10) - 1] 16872 }); 16873 }, 16874 16875 u : function(dom, node) { 16876 replaceWithSpan(node, { 16877 textDecoration : 'underline' 16878 }); 16879 }, 16880 16881 strike : function(dom, node) { 16882 replaceWithSpan(node, { 16883 textDecoration : 'line-through' 16884 }); 16885 } 16886 }; 16887 16888 ed.onPreProcess.add(convert); 16889 ed.onSetContent.add(convert); 16890 16891 ed.onInit.add(function() { 16892 ed.selection.onSetContent.add(convert); 16893 }); 16894 } 16895 }); 16896 16897 (function(tinymce) { 16898 var TreeWalker = tinymce.dom.TreeWalker; 16899 16900 tinymce.EnterKey = function(editor) { 16901 var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager, nonEmptyElementsMap = editor.schema.getNonEmptyElements(); 16902 16903 function handleEnterKey(evt) { 16904 var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, 16905 newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; 16906 16907 // Returns true if the block can be split into two blocks or not 16908 function canSplitBlock(node) { 16909 return node && 16910 dom.isBlock(node) && 16911 !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && 16912 !/^(fixed|absolute)/i.test(node.style.position) && 16913 dom.getContentEditable(node) !== "true"; 16914 }; 16915 16916 // Renders empty block on IE 16917 function renderBlockOnIE(block) { 16918 var oldRng; 16919 16920 if (tinymce.isIE && dom.isBlock(block)) { 16921 oldRng = selection.getRng(); 16922 block.appendChild(dom.create('span', null, '\u00a0')); 16923 selection.select(block); 16924 block.lastChild.outerHTML = ''; 16925 selection.setRng(oldRng); 16926 } 16927 }; 16928 16929 // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> 16930 function trimInlineElementsOnLeftSideOfBlock(block) { 16931 var node = block, firstChilds = [], i; 16932 16933 // Find inner most first child ex: <p><i><b>*</b></i></p> 16934 while (node = node.firstChild) { 16935 if (dom.isBlock(node)) { 16936 return; 16937 } 16938 16939 if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 16940 firstChilds.push(node); 16941 } 16942 } 16943 16944 i = firstChilds.length; 16945 while (i--) { 16946 node = firstChilds[i]; 16947 if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { 16948 dom.remove(node); 16949 } 16950 } 16951 }; 16952 16953 // Moves the caret to a suitable position within the root for example in the first non pure whitespace text node or before an image 16954 function moveToCaretPosition(root) { 16955 var walker, node, rng, y, viewPort, lastNode = root, tempElm; 16956 16957 rng = dom.createRng(); 16958 16959 if (root.hasChildNodes()) { 16960 walker = new TreeWalker(root, root); 16961 16962 while (node = walker.current()) { 16963 if (node.nodeType == 3) { 16964 rng.setStart(node, 0); 16965 rng.setEnd(node, 0); 16966 break; 16967 } 16968 16969 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 16970 rng.setStartBefore(node); 16971 rng.setEndBefore(node); 16972 break; 16973 } 16974 16975 lastNode = node; 16976 node = walker.next(); 16977 } 16978 16979 if (!node) { 16980 rng.setStart(lastNode, 0); 16981 rng.setEnd(lastNode, 0); 16982 } 16983 } else { 16984 if (root.nodeName == 'BR') { 16985 if (root.nextSibling && dom.isBlock(root.nextSibling)) { 16986 // Trick on older IE versions to render the caret before the BR between two lists 16987 if (!documentMode || documentMode < 9) { 16988 tempElm = dom.create('br'); 16989 root.parentNode.insertBefore(tempElm, root); 16990 } 16991 16992 rng.setStartBefore(root); 16993 rng.setEndBefore(root); 16994 } else { 16995 rng.setStartAfter(root); 16996 rng.setEndAfter(root); 16997 } 16998 } else { 16999 rng.setStart(root, 0); 17000 rng.setEnd(root, 0); 17001 } 17002 } 17003 17004 selection.setRng(rng); 17005 17006 // Remove tempElm created for old IE:s 17007 dom.remove(tempElm); 17008 17009 viewPort = dom.getViewPort(editor.getWin()); 17010 17011 // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs 17012 y = dom.getPos(root).y; 17013 if (y < viewPort.y || y + 25 > viewPort.y + viewPort.h) { 17014 editor.getWin().scrollTo(0, y < viewPort.y ? y : y - viewPort.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks 17015 } 17016 }; 17017 17018 // Creates a new block element by cloning the current one or creating a new one if the name is specified 17019 // This function will also copy any text formatting from the parent block and add it to the new one 17020 function createNewBlock(name) { 17021 var node = container, block, clonedNode, caretNode; 17022 17023 block = name || parentBlockName == "TABLE" ? dom.create(name || newBlockName) : parentBlock.cloneNode(false); 17024 caretNode = block; 17025 17026 // Clone any parent styles 17027 if (settings.keep_styles !== false) { 17028 do { 17029 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) { 17030 clonedNode = node.cloneNode(false); 17031 dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique 17032 17033 if (block.hasChildNodes()) { 17034 clonedNode.appendChild(block.firstChild); 17035 block.appendChild(clonedNode); 17036 } else { 17037 caretNode = clonedNode; 17038 block.appendChild(clonedNode); 17039 } 17040 } 17041 } while (node = node.parentNode); 17042 } 17043 17044 // BR is needed in empty blocks on non IE browsers 17045 if (!tinymce.isIE) { 17046 caretNode.innerHTML = '<br>'; 17047 } 17048 17049 return block; 17050 }; 17051 17052 // Returns true/false if the caret is at the start/end of the parent block element 17053 function isCaretAtStartOrEndOfBlock(start) { 17054 var walker, node, name; 17055 17056 // Caret is in the middle of a text node like "a|b" 17057 if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) { 17058 return false; 17059 } 17060 17061 // If after the last element in block node edge case for #5091 17062 if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { 17063 return true; 17064 } 17065 17066 // If the caret if before the first element in parentBlock 17067 if (start && container.nodeType == 1 && container == parentBlock.firstChild) { 17068 return true; 17069 } 17070 17071 // Caret can be before/after a table 17072 if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) { 17073 return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); 17074 } 17075 17076 // Walk the DOM and look for text nodes or non empty elements 17077 walker = new TreeWalker(container, parentBlock); 17078 17079 // If caret is in beginning or end of a text block then jump to the next/previous node 17080 if (container.nodeType == 3) { 17081 if (start && offset == 0) { 17082 walker.prev(); 17083 } else if (!start && offset == container.nodeValue.length) { 17084 walker.next(); 17085 } 17086 } 17087 17088 while (node = walker.current()) { 17089 if (node.nodeType === 1) { 17090 // Ignore bogus elements 17091 if (!node.getAttribute('data-mce-bogus')) { 17092 // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p> 17093 name = node.nodeName.toLowerCase(); 17094 if (nonEmptyElementsMap[name] && name !== 'br') { 17095 return false; 17096 } 17097 } 17098 } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { 17099 return false; 17100 } 17101 17102 if (start) { 17103 walker.prev(); 17104 } else { 17105 walker.next(); 17106 } 17107 } 17108 17109 return true; 17110 }; 17111 17112 // Wraps any text nodes or inline elements in the specified forced root block name 17113 function wrapSelfAndSiblingsInDefaultBlock(container, offset) { 17114 var newBlock, parentBlock, startNode, node, next, blockName = newBlockName || 'P'; 17115 17116 // Not in a block element or in a table cell or caption 17117 parentBlock = dom.getParent(container, dom.isBlock); 17118 if (!parentBlock || !canSplitBlock(parentBlock)) { 17119 parentBlock = parentBlock || editableRoot; 17120 17121 if (!parentBlock.hasChildNodes()) { 17122 newBlock = dom.create(blockName); 17123 parentBlock.appendChild(newBlock); 17124 rng.setStart(newBlock, 0); 17125 rng.setEnd(newBlock, 0); 17126 return newBlock; 17127 } 17128 17129 // Find parent that is the first child of parentBlock 17130 node = container; 17131 while (node.parentNode != parentBlock) { 17132 node = node.parentNode; 17133 } 17134 17135 // Loop left to find start node start wrapping at 17136 while (node && !dom.isBlock(node)) { 17137 startNode = node; 17138 node = node.previousSibling; 17139 } 17140 17141 if (startNode) { 17142 newBlock = dom.create(blockName); 17143 startNode.parentNode.insertBefore(newBlock, startNode); 17144 17145 // Start wrapping until we hit a block 17146 node = startNode; 17147 while (node && !dom.isBlock(node)) { 17148 next = node.nextSibling; 17149 newBlock.appendChild(node); 17150 node = next; 17151 } 17152 17153 // Restore range to it's past location 17154 rng.setStart(container, offset); 17155 rng.setEnd(container, offset); 17156 } 17157 } 17158 17159 return container; 17160 }; 17161 17162 // Inserts a block or br before/after or in the middle of a split list of the LI is empty 17163 function handleEmptyListItem() { 17164 function isFirstOrLastLi(first) { 17165 var node = containerBlock[first ? 'firstChild' : 'lastChild']; 17166 17167 // Find first/last element since there might be whitespace there 17168 while (node) { 17169 if (node.nodeType == 1) { 17170 break; 17171 } 17172 17173 node = node[first ? 'nextSibling' : 'previousSibling']; 17174 } 17175 17176 return node === parentBlock; 17177 }; 17178 17179 newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); 17180 17181 if (isFirstOrLastLi(true) && isFirstOrLastLi()) { 17182 // Is first and last list item then replace the OL/UL with a text block 17183 dom.replace(newBlock, containerBlock); 17184 } else if (isFirstOrLastLi(true)) { 17185 // First LI in list then remove LI and add text block before list 17186 containerBlock.parentNode.insertBefore(newBlock, containerBlock); 17187 } else if (isFirstOrLastLi()) { 17188 // Last LI in list then temove LI and add text block after list 17189 dom.insertAfter(newBlock, containerBlock); 17190 renderBlockOnIE(newBlock); 17191 } else { 17192 // Middle LI in list the split the list and insert a text block in the middle 17193 // Extract after fragment and insert it after the current block 17194 tmpRng = rng.cloneRange(); 17195 tmpRng.setStartAfter(parentBlock); 17196 tmpRng.setEndAfter(containerBlock); 17197 fragment = tmpRng.extractContents(); 17198 dom.insertAfter(fragment, containerBlock); 17199 dom.insertAfter(newBlock, containerBlock); 17200 } 17201 17202 dom.remove(parentBlock); 17203 moveToCaretPosition(newBlock); 17204 undoManager.add(); 17205 }; 17206 17207 // Walks the parent block to the right and look for BR elements 17208 function hasRightSideBr() { 17209 var walker = new TreeWalker(container, parentBlock), node; 17210 17211 while (node = walker.current()) { 17212 if (node.nodeName == 'BR') { 17213 return true; 17214 } 17215 17216 node = walker.next(); 17217 } 17218 } 17219 17220 // Inserts a BR element if the forced_root_block option is set to false or empty string 17221 function insertBr() { 17222 var brElm, extraBr; 17223 17224 if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { 17225 // Insert extra BR element at the end block elements 17226 if (!tinymce.isIE && !hasRightSideBr()) { 17227 brElm = dom.create('br') 17228 rng.insertNode(brElm); 17229 rng.setStartAfter(brElm); 17230 rng.setEndAfter(brElm); 17231 extraBr = true; 17232 } 17233 } 17234 17235 brElm = dom.create('br'); 17236 rng.insertNode(brElm); 17237 17238 // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it 17239 if (tinymce.isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { 17240 brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); 17241 } 17242 17243 if (!extraBr) { 17244 rng.setStartAfter(brElm); 17245 rng.setEndAfter(brElm); 17246 } else { 17247 rng.setStartBefore(brElm); 17248 rng.setEndBefore(brElm); 17249 } 17250 17251 selection.setRng(rng); 17252 undoManager.add(); 17253 }; 17254 17255 // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element 17256 function trimLeadingLineBreaks(node) { 17257 do { 17258 if (node.nodeType === 3) { 17259 node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); 17260 } 17261 17262 node = node.firstChild; 17263 } while (node); 17264 }; 17265 17266 function getEditableRoot(node) { 17267 var root = dom.getRoot(), parent, editableRoot; 17268 17269 // Get all parents until we hit a non editable parent or the root 17270 parent = node; 17271 while (parent !== root && dom.getContentEditable(parent) !== "false") { 17272 if (dom.getContentEditable(parent) === "true") { 17273 editableRoot = parent; 17274 } 17275 17276 parent = parent.parentNode; 17277 } 17278 17279 return parent !== root ? editableRoot : root; 17280 }; 17281 17282 // Adds a BR at the end of blocks that only contains an IMG or INPUT since these might be floated and then they won't expand the block 17283 function addBrToBlockIfNeeded(block) { 17284 var lastChild; 17285 17286 // IE will render the blocks correctly other browsers needs a BR 17287 if (!tinymce.isIE) { 17288 block.normalize(); // Remove empty text nodes that got left behind by the extract 17289 17290 // Check if the block is empty or contains a floated last child 17291 lastChild = block.lastChild; 17292 if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { 17293 dom.add(block, 'br'); 17294 } 17295 } 17296 }; 17297 17298 // Delete any selected contents 17299 if (!rng.collapsed) { 17300 editor.execCommand('Delete'); 17301 return; 17302 } 17303 17304 // Event is blocked by some other handler for example the lists plugin 17305 if (evt.isDefaultPrevented()) { 17306 return; 17307 } 17308 17309 // Setup range items and newBlockName 17310 container = rng.startContainer; 17311 offset = rng.startOffset; 17312 newBlockName = settings.forced_root_block; 17313 newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; 17314 documentMode = dom.doc.documentMode; 17315 17316 // Resolve node index 17317 if (container.nodeType == 1 && container.hasChildNodes()) { 17318 isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 17319 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 17320 if (isAfterLastNodeInContainer && container.nodeType == 3) { 17321 offset = container.nodeValue.length; 17322 } else { 17323 offset = 0; 17324 } 17325 } 17326 17327 // Get editable root node normaly the body element but sometimes a div or span 17328 editableRoot = getEditableRoot(container); 17329 17330 // If there is no editable root then enter is done inside a contentEditable false element 17331 if (!editableRoot) { 17332 return; 17333 } 17334 17335 undoManager.beforeChange(); 17336 17337 // If editable root isn't block nor the root of the editor 17338 if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { 17339 if (!newBlockName || evt.shiftKey) { 17340 insertBr(); 17341 } 17342 17343 return; 17344 } 17345 17346 // Wrap the current node and it's sibling in a default block if it's needed. 17347 // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> 17348 // This won't happen if root blocks are disabled or the shiftKey is pressed 17349 if ((newBlockName && !evt.shiftKey) || (!newBlockName && evt.shiftKey)) { 17350 container = wrapSelfAndSiblingsInDefaultBlock(container, offset); 17351 } 17352 17353 // Find parent block and setup empty block paddings 17354 parentBlock = dom.getParent(container, dom.isBlock); 17355 containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 17356 17357 // Setup block names 17358 parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17359 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17360 17361 // Handle enter inside an empty list item 17362 if (parentBlockName == 'LI' && dom.isEmpty(parentBlock)) { 17363 // Let the list plugin or browser handle nested lists for now 17364 if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { 17365 return false; 17366 } 17367 17368 handleEmptyListItem(); 17369 return; 17370 } 17371 17372 // Don't split PRE tags but insert a BR instead easier when writing code samples etc 17373 if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { 17374 if (!evt.shiftKey) { 17375 insertBr(); 17376 return; 17377 } 17378 } else { 17379 // If no root block is configured then insert a BR by default or if the shiftKey is pressed 17380 if ((!newBlockName && !evt.shiftKey && parentBlockName != 'LI') || (newBlockName && evt.shiftKey)) { 17381 insertBr(); 17382 return; 17383 } 17384 } 17385 17386 // Default block name if it's not configured 17387 newBlockName = newBlockName || 'P'; 17388 17389 // Insert new block before/after the parent block depending on caret location 17390 if (isCaretAtStartOrEndOfBlock()) { 17391 // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup 17392 if (/^(H[1-6]|PRE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') { 17393 newBlock = createNewBlock(newBlockName); 17394 } else { 17395 newBlock = createNewBlock(); 17396 } 17397 17398 // Split the current container block element if enter is pressed inside an empty inner block element 17399 if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) { 17400 // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P 17401 newBlock = dom.split(containerBlock, parentBlock); 17402 } else { 17403 dom.insertAfter(newBlock, parentBlock); 17404 } 17405 17406 moveToCaretPosition(newBlock); 17407 } else if (isCaretAtStartOrEndOfBlock(true)) { 17408 // Insert new block before 17409 newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); 17410 renderBlockOnIE(newBlock); 17411 } else { 17412 // Extract after fragment and insert it after the current block 17413 tmpRng = rng.cloneRange(); 17414 tmpRng.setEndAfter(parentBlock); 17415 fragment = tmpRng.extractContents(); 17416 trimLeadingLineBreaks(fragment); 17417 newBlock = fragment.firstChild; 17418 dom.insertAfter(fragment, parentBlock); 17419 trimInlineElementsOnLeftSideOfBlock(newBlock); 17420 addBrToBlockIfNeeded(parentBlock); 17421 moveToCaretPosition(newBlock); 17422 } 17423 17424 dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique 17425 undoManager.add(); 17426 } 17427 17428 editor.onKeyDown.add(function(ed, evt) { 17429 if (evt.keyCode == 13) { 17430 if (handleEnterKey(evt) !== false) { 17431 evt.preventDefault(); 17432 } 17433 } 17434 }); 17435 }; 17436 })(tinymce); 17437 17438